diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9563be6125..79126fd658 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,8 +5,7 @@ "immich-server", "redis", "database", - "immich-machine-learning", - "init" + "immich-machine-learning" ], "dockerComposeFile": [ "../docker/docker-compose.dev.yml", diff --git a/.devcontainer/mobile/container-compose-overrides.yml b/.devcontainer/mobile/container-compose-overrides.yml index c0655e50e7..d6cd95018f 100644 --- a/.devcontainer/mobile/container-compose-overrides.yml +++ b/.devcontainer/mobile/container-compose-overrides.yml @@ -12,7 +12,6 @@ services: - server_node_modules:/workspaces/immich/server/node_modules - web_node_modules:/workspaces/immich/web/node_modules - ${UPLOAD_LOCATION}/photos:/data - - ${UPLOAD_LOCATION}/photos/upload:/data/upload - /etc/localtime:/etc/localtime:ro database: diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index 539caa0dd1..3be5cd8f3f 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -8,8 +8,7 @@ services: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ volumes: !override - ..:/workspaces/immich - - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload + - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - /etc/localtime:/etc/localtime:ro - pnpm-store:/usr/src/app/.pnpm-store - server-node_modules:/usr/src/app/server/node_modules @@ -24,9 +23,6 @@ services: - coverage:/usr/src/app/web/coverage immich-web: env_file: !reset [] - init: - env_file: !reset [] - command: sh -c 'for path in /data /data/upload /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' immich-machine-learning: env_file: !reset [] database: @@ -42,7 +38,5 @@ services: redis: env_file: !reset [] volumes: - # Node modules for each service to avoid conflicts and ensure consistent dependencies - upload1-devcontainer-volume: - upload2-devcontainer-volume: + upload-devcontainer-volume: postgres-devcontainer-volume: diff --git a/.devcontainer/server/container-start-backend.sh b/.devcontainer/server/container-start-backend.sh index d0176a7d66..35fa60f89b 100755 --- a/.devcontainer/server/container-start-backend.sh +++ b/.devcontainer/server/container-start-backend.sh @@ -11,7 +11,7 @@ run_cmd pnpm --filter immich install log "Starting Nest API Server" log "" cd "${IMMICH_WORKSPACE}/server" || ( - log "Immich workspace not found"jj + log "Immich workspace not found" exit 1 ) diff --git a/.github/.nvmrc b/.github/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/.github/labeler.yml b/.github/labeler.yml index c0c52f1d7e..d8923a3035 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -6,7 +6,6 @@ cli: documentation: - changed-files: - any-glob-to-any-file: - - docs/blob/** - docs/docs/** - docs/src/** - docs/static/** diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index aa756a7d08..0bd3b30814 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -34,3 +34,7 @@ The `/api/something` endpoint is now `/api/something-else` - [ ] I have followed naming conventions/patterns in the surrounding code - [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc. - [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`) + +## Please describe to which degree, if any, an LLM was used in creating this pull request. + +... diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 71fa358942..0996c8eccb 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -32,24 +32,18 @@ jobs: permissions: contents: read outputs: - should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run: ${{ steps.check.outputs.should_run }} steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - - id: found_paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - name: Check what should run + id: check + uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0 with: filters: | mobile: - 'mobile/**' - workflow: - - '.github/workflows/build-mobile.yml' - - name: Check if we should force jobs to run - id: should_force - run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT" + force-filters: | + - '.github/workflows/build-mobile.yml' + force-events: 'workflow_call,workflow_dispatch' build-sign-android: name: Build and sign Android @@ -57,7 +51,7 @@ jobs: permissions: contents: read # Skip when PR from a fork - if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }} + if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} runs-on: mich steps: @@ -79,7 +73,7 @@ jobs: - name: Restore Gradle Cache id: cache-gradle-restore - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | ~/.gradle/caches @@ -136,7 +130,7 @@ jobs: - name: Save Gradle Cache id: cache-gradle-save - uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 if: github.ref == 'refs/heads/main' with: path: | diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index 1cc646961b..8470e0e18c 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -8,8 +8,18 @@ name: Close likely duplicates permissions: {} jobs: + should_run: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.should_run.outputs.run }} + steps: + - id: should_run + run: echo "run=${{ github.event_name == 'issues' || github.event.discussion.category.name == 'Feature Request' }}" >> $GITHUB_OUTPUT + get_body: runs-on: ubuntu-latest + needs: should_run + if: ${{ needs.should_run.outputs.should_run == 'true' }} env: EVENT: ${{ toJSON(github.event) }} outputs: @@ -22,23 +32,24 @@ jobs: get_checkbox_json: runs-on: ubuntu-latest - needs: get_body + needs: [get_body, should_run] + if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: yshavit/mdq:0.8.0@sha256:c69224d34224a0043d9a3ee46679ba4a2a25afaac445f293d92afe13cd47fcea + image: ghcr.io/immich-app/mdq:main@sha256:d8ae47cf2e6cf4e2559bd57a60b73674fe44f897cba2c2bddff2987a05be10a4 outputs: - json: ${{ steps.get_checkbox.outputs.json }} + checked: ${{ steps.get_checkbox.outputs.checked }} steps: - id: get_checkbox env: BODY: ${{ needs.get_body.outputs.body }} run: | - JSON=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes') - echo "json=$JSON" >> $GITHUB_OUTPUT + CHECKED=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes' | jq '.items[0].list[0].checked // false') + echo "checked=$CHECKED" >> $GITHUB_OUTPUT close_and_comment: runs-on: ubuntu-latest - needs: get_checkbox_json - if: ${{ !fromJSON(needs.get_checkbox_json.outputs.json).items[0].list[0].checked }} + needs: [get_checkbox_json, should_run] + if: ${{ needs.should_run.outputs.should_run == 'true' && needs.get_checkbox_json.outputs.checked != 'true' }} permissions: issues: write discussions: write diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fac8afd8ae..503dd30d9a 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@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 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@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 # ℹ️ 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@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6e2bcdb84d..09528346fc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -20,15 +20,11 @@ jobs: permissions: contents: read outputs: - should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run: ${{ steps.check.outputs.should_run }} steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - id: found_paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - name: Check what should run + id: check + uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0 with: filters: | server: @@ -38,14 +34,11 @@ jobs: - 'i18n/**' machine-learning: - 'machine-learning/**' - workflow: - - '.github/workflows/docker.yml' - - '.github/workflows/multi-runner-build.yml' - - '.github/actions/image-build' - - - name: Check if we should force jobs to run - id: should_force - run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" + force-filters: | + - '.github/workflows/docker.yml' + - '.github/workflows/multi-runner-build.yml' + - '.github/actions/image-build' + force-events: 'workflow_dispatch,release' retag_ml: name: Re-Tag ML @@ -53,7 +46,7 @@ jobs: permissions: contents: read packages: write - if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == false && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest strategy: matrix: @@ -82,7 +75,7 @@ jobs: permissions: contents: read packages: write - if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest strategy: matrix: @@ -108,7 +101,7 @@ jobs: machine-learning: name: Build and Push ML needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }} strategy: fail-fast: false matrix: @@ -153,7 +146,7 @@ jobs: server: name: Build and Push Server needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 permissions: contents: read diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 2514ee8639..0879c30386 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -18,32 +18,28 @@ jobs: permissions: contents: read outputs: - should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.found_paths.outputs.open-api == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run: ${{ steps.check.outputs.should_run }} steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - id: found_paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - name: Check what should run + id: check + uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0 with: filters: | docs: - 'docs/**' - workflow: - - '.github/workflows/docs-build.yml' open-api: - 'open-api/immich-openapi-specs.json' - - name: Check if we should force jobs to run - id: should_force - run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT" + force-filters: | + - '.github/workflows/docs-build.yml' + force-events: 'release' + force-branches: 'main' build: name: Docs Build needs: pre-job permissions: contents: read - if: ${{ needs.pre-job.outputs.should_run == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).docs == true }} runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index c05121ad70..b504b811e3 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -20,7 +20,7 @@ jobs: run: echo 'The triggering workflow did not succeed' && exit 1 - name: Get artifact id: get-artifact - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -38,7 +38,7 @@ jobs: return { found: true, id: matchArtifact.id }; - name: Determine deploy parameters id: parameters - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: HEAD_SHA: ${{ github.event.workflow_run.head_sha }} with: @@ -114,7 +114,7 @@ jobs: - name: Load parameters id: parameters - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: PARAM_JSON: ${{ needs.checks.outputs.parameters }} with: @@ -125,7 +125,7 @@ jobs: core.setOutput("shouldDeploy", parameters.shouldDeploy); - name: Download artifact - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }} with: diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 4c7c57e4f0..849de79a47 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -28,6 +28,9 @@ jobs: token: ${{ steps.generate-token.outputs.token }} persist-credentials: true + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: @@ -45,7 +48,7 @@ jobs: message: 'chore: fix formatting' - name: Remove label - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 if: always() with: script: | diff --git a/.github/workflows/merge-translations.yml b/.github/workflows/merge-translations.yml new file mode 100644 index 0000000000..d494460320 --- /dev/null +++ b/.github/workflows/merge-translations.yml @@ -0,0 +1,128 @@ +name: Merge translations + +on: + workflow_dispatch: + workflow_call: + secrets: + PUSH_O_MATIC_APP_ID: + required: true + PUSH_O_MATIC_APP_KEY: + required: true + WEBLATE_TOKEN: + required: true + inputs: + skip: + description: 'Skip translations' + required: false + type: boolean + +permissions: {} + +env: + WEBLATE_HOST: 'https://hosted.weblate.org' + WEBLATE_COMPONENT: 'immich/immich' + +jobs: + merge: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Find translation PR + id: find_pr + if: ${{ inputs.skip != true }} + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + PR=$(gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable) + echo "$PR" + + PR_NUMBER=$(echo "$PR" | jq ' + if length == 1 then + .[0].number + else + error("Expected exactly 1 entry, got \(length)") + end + ' 2>&1) || exit 1 + + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "Selected PR $PR_NUMBER" + + if ! echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"'; then + echo "PR is not mergeable" + exit 1 + fi + + - name: Generate a token + id: generate_token + if: ${{ inputs.skip != true }} + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + + - name: Lock weblate + if: ${{ inputs.skip != true }} + env: + WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }} + run: | + curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=true + + - name: Commit translations + if: ${{ inputs.skip != true }} + env: + WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }} + run: | + curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=commit + curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=push + + - name: Merge PR + id: merge_pr + if: ${{ inputs.skip != true }} + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }} + run: | + set -euo pipefail + + REVIEW_ID=$(gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \ + | jq '.id') + echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT + gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash + + - name: Wait for PR to merge + if: ${{ inputs.skip != true }} + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }} + REVIEW_ID: ${{ steps.merge_pr.outputs.REVIEW_ID }} + run: | + # So we clean up no matter what + set +e + + for i in {1..100}; do + if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state | jq -e '.state == "MERGED"'; then + echo "PR merged" + exit 0 + else + echo "PR not merged yet, waiting..." + sleep 6 + fi + done + echo "PR did not merge in time" + gh api -X PUT "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews/$REVIEW_ID/dismissals" --field message='Merge attempt timed out' --field event='DISMISS' + gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --disable-auto + exit 1 + + - name: Unlock weblate + if: ${{ inputs.skip != true }} + env: + WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }} + run: | + curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=false + + - name: Report success + run: | + echo "Workflow completed successfully (or was skipped)" diff --git a/.github/workflows/org-checks.yml b/.github/workflows/org-checks.yml deleted file mode 100644 index 9781dc3b83..0000000000 --- a/.github/workflows/org-checks.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Org Checks - -on: - pull_request_review: - pull_request: - -jobs: - check-approvals: - name: Check for Team/Admin Review - uses: immich-app/devtools/.github/workflows/required-approval.yml@main - permissions: - pull-requests: read - contents: read diff --git a/.github/workflows/org-pr-require-conventional-commit.yml b/.github/workflows/org-pr-require-conventional-commit.yml new file mode 100644 index 0000000000..5e5f84ef39 --- /dev/null +++ b/.github/workflows/org-pr-require-conventional-commit.yml @@ -0,0 +1,12 @@ +name: PR Conventional Commit + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + validate-pr-title: + name: Validate PR Title (conventional commit) + uses: immich-app/devtools/.github/workflows/shared-pr-require-conventional-commit.yml@main + permissions: + pull-requests: write diff --git a/.github/workflows/org-zizmor.yml b/.github/workflows/org-zizmor.yml new file mode 100644 index 0000000000..8510fd85b4 --- /dev/null +++ b/.github/workflows/org-zizmor.yml @@ -0,0 +1,15 @@ +name: Zizmor + +on: + pull_request: + push: + branches: [main] + +jobs: + zizmor: + name: Zizmor + uses: immich-app/devtools/.github/workflows/shared-zizmor.yml@main + permissions: + actions: read + contents: read + security-events: write diff --git a/.github/workflows/pr-require-conventional-commit.yml b/.github/workflows/pr-require-conventional-commit.yml deleted file mode 100644 index 78ba77495c..0000000000 --- a/.github/workflows/pr-require-conventional-commit.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: PR Conventional Commit Validation - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: {} - -jobs: - validate-pr-title: - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - name: PR Conventional Commit Validation - uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0 - with: - task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' - add_label: 'false' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 70882ea201..8b6dc0af1c 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -10,12 +10,17 @@ on: type: choice options: - 'false' + - major - minor - patch mobileBump: description: 'Bump mobile build number' required: false type: boolean + skipTranslations: + description: 'Skip translations' + required: false + type: boolean concurrency: group: ${{ github.workflow }}-${{ github.ref }}-root @@ -24,15 +29,27 @@ concurrency: permissions: {} jobs: + merge_translations: + uses: ./.github/workflows/merge-translations.yml + with: + skip: ${{ inputs.skipTranslations }} + permissions: + pull-requests: write + secrets: + PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }} + PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }} + bump_version: runs-on: ubuntu-latest + needs: [merge_translations] outputs: ref: ${{ steps.push-tag.outputs.commit_long_sha }} permissions: {} # No job-level permissions are needed because it uses the app-token steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -42,6 +59,7 @@ jobs: with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: true + ref: main - name: Install uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 @@ -93,7 +111,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -110,7 +128,7 @@ jobs: name: release-apk-signed - name: Create draft release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 with: draft: true tag_name: ${{ env.IMMICH_VERSION }} diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml index 3ab9fd267f..1d9a0060ad 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -24,7 +24,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | github.rest.issues.removeLabel({ diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index d9b8c1fc0d..d30f95422c 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -17,28 +17,23 @@ jobs: permissions: contents: read outputs: - should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run: ${{ steps.check.outputs.should_run }} steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - id: found_paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - name: Check what should run + id: check + uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0 with: filters: | mobile: - 'mobile/**' - workflow: - - '.github/workflows/static_analysis.yml' - - name: Check if we should force jobs to run - id: should_force - run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" + force-filters: | + - '.github/workflows/static_analysis.yml' + force-events: 'workflow_dispatch,release' mobile-dart-analyze: name: Run Dart Code Analysis needs: pre-job - if: ${{ needs.pre-job.outputs.should_run == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).mobile == true }} runs-on: ubuntu-latest permissions: contents: read @@ -100,36 +95,10 @@ jobs: - name: Run dart format run: make format - - name: Run dart custom_lint - run: dart run custom_lint + # TODO: Re-enable after upgrading custom_lint + # - name: Run dart custom_lint + # run: dart run custom_lint # TODO: Use https://github.com/CQLabs/dcm-action - name: Run DCM run: dcm analyze lib --fatal-style --fatal-warnings - - zizmor: - name: zizmor - runs-on: ubuntu-latest - permissions: - security-events: write - contents: read - actions: read - steps: - - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - - name: Install the latest version of uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - - - name: Run zizmor 🌈 - run: uvx zizmor --format=sarif . > results.sarif - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 - with: - sarif_file: results.sarif - category: zizmor diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39b4b12b1a..ffc5b41f73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,23 +14,11 @@ jobs: permissions: contents: read outputs: - should_run_i18n: ${{ steps.found_paths.outputs.i18n == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_e2e: ${{ steps.found_paths.outputs.e2e == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_mobile: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} - should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed + should_run: ${{ steps.check.outputs.should_run }} steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - id: found_paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - name: Check what should run + id: check + uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0 with: filters: | i18n: @@ -50,17 +38,16 @@ jobs: - 'mobile/**' machine-learning: - 'machine-learning/**' - workflow: - - '.github/workflows/test.yml' .github: - '.github/**' - - name: Check if we should force jobs to run - id: should_force - run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT" + force-filters: | + - '.github/workflows/test.yml' + force-events: 'workflow_dispatch' + server-unit-tests: name: Test & Lint Server needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} runs-on: ubuntu-latest permissions: contents: read @@ -97,7 +84,7 @@ jobs: cli-unit-tests: name: Unit Test CLI needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).cli == true }} runs-on: ubuntu-latest permissions: contents: read @@ -137,7 +124,7 @@ jobs: cli-unit-tests-win: name: Unit Test CLI (Windows) needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).cli == true }} runs-on: windows-latest permissions: contents: read @@ -172,7 +159,7 @@ jobs: web-lint: name: Lint Web needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).web == true }} runs-on: mich permissions: contents: read @@ -209,7 +196,7 @@ jobs: web-unit-tests: name: Test Web needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).web == true }} runs-on: ubuntu-latest permissions: contents: read @@ -243,7 +230,7 @@ jobs: i18n-tests: name: Test i18n needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_i18n == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} runs-on: ubuntu-latest permissions: contents: read @@ -281,7 +268,7 @@ jobs: e2e-tests-lint: name: End-to-End Lint needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true }} runs-on: ubuntu-latest permissions: contents: read @@ -320,7 +307,7 @@ jobs: server-medium-tests: name: Medium Tests (Server) needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} runs-on: ubuntu-latest permissions: contents: read @@ -348,7 +335,7 @@ jobs: e2e-tests-server-cli: name: End-to-End Tests (Server & CLI) needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true }} runs-on: ${{ matrix.runner }} permissions: contents: read @@ -396,7 +383,7 @@ jobs: e2e-tests-web: name: End-to-End Tests (Web) needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }} runs-on: ${{ matrix.runner }} permissions: contents: read @@ -449,7 +436,7 @@ jobs: mobile-unit-tests: name: Unit Test Mobile needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).mobile == true }} runs-on: ubuntu-latest permissions: contents: read @@ -471,7 +458,7 @@ jobs: ml-unit-tests: name: Unit Test ML needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }} runs-on: ubuntu-latest permissions: contents: read @@ -507,7 +494,7 @@ jobs: github-files-formatting: name: .github Files Formatting needs: pre-job - if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run)['.github'] == true }} runs-on: ubuntu-latest permissions: contents: read @@ -569,7 +556,8 @@ jobs: - name: Build the app run: pnpm --filter immich build - name: Run API generation - run: make open-api + run: ./bin/generate-open-api.sh + working-directory: open-api - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files @@ -593,7 +581,7 @@ jobs: contents: read services: postgres: - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:ec713143dca1a426eba2e03707c319e2ec3cc9d304ef767f777f8e297dee820c + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:da52bbead5d818adaa8077c8dcdaad0aaf93038c31ad8348b51f9f0ec1310a4d env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index f732fe2a49..d7deb244f9 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -3,48 +3,52 @@ name: Weblate checks on: pull_request: branches: [main] + types: + - opened + - synchronize + - ready_for_review + - auto_merge_enabled + - auto_merge_disabled permissions: {} +env: + BOT_NAME: immich-push-o-matic + jobs: pre-job: runs-on: ubuntu-latest permissions: contents: read outputs: - should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}} + should_run: ${{ steps.check.outputs.should_run }} steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - id: found_paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - name: Check what should run + id: check + uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0 with: filters: | i18n: - 'i18n/!(en)**\.json' + exclude-branches: 'chore/translations' + skip-force-logic: 'true' enforce-lock: name: Check Weblate Lock needs: [pre-job] runs-on: ubuntu-latest permissions: {} - if: ${{ needs.pre-job.outputs.should_run == 'true' }} + if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} steps: - - name: Check weblate lock + - name: Bot review status + env: + PR_NUMBER: ${{ github.event.pull_request.number || github.event.pull_request_review.pull_request.number }} + GH_TOKEN: ${{ github.token }} run: | - if [[ "false" = $(curl https://hosted.weblate.org/api/components/immich/immich/lock/ | jq .locked) ]]; then - exit 1 - fi - - name: Find Pull Request - uses: juliangruber/find-pull-request-action@952b3bb1ddb2dcc0aa3479e98bb1c2d1a922f096 # v1.10.0 - id: find-pr - with: - branch: chore/translations - - name: Fail if existing weblate PR - if: ${{ steps.find-pr.outputs.number }} - run: exit 1 + # Then check for APPROVED by the bot, if absent fail + gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == env.BOT_NAME and .state == "APPROVED")) | length > 0' \ + || (echo "The push-o-matic bot has not approved this PR yet" && exit 1) + success-check-lock: name: Weblate Lock Check Success needs: [enforce-lock] diff --git a/.gitignore b/.gitignore index af85d96c02..3220701cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ mobile/libisar.dylib mobile/openapi/test mobile/openapi/doc mobile/openapi/.openapi-generator/FILES +mobile/ios/build open-api/typescript-sdk/build mobile/android/fastlane/report.xml @@ -25,3 +26,5 @@ mobile/ios/fastlane/report.xml vite.config.js.timestamp-* .pnpm-store +.devcontainer/library +.devcontainer/.env* diff --git a/CODEOWNERS b/CODEOWNERS index cd61814ff8..8759cf2357 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,5 +1,7 @@ /.github/ @bo0tzz /docker/ @bo0tzz /server/ @danieldietzler +/web/ @danieldietzler /machine-learning/ @mertalev /e2e/ @danieldietzler +/mobile/ @shenlong-tanwen diff --git a/Makefile b/Makefile index 31a00ee6be..34fb408c41 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,29 @@ -dev: prepare-volumes +dev: @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans dev-down: docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans -dev-update: prepare-volumes +dev-update: @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans -dev-scale: prepare-volumes +dev-scale: @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans -dev-docs: prepare-volumes +dev-docs: npm --prefix docs run start .PHONY: e2e -e2e: prepare-volumes +e2e: @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans -e2e-update: prepare-volumes +e2e-update: @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans e2e-down: docker compose -f ./e2e/docker-compose.yml down --remove-orphans -prod: +prod: @trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans prod-down: @@ -33,16 +33,16 @@ prod-scale: @trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans .PHONY: open-api -open-api: prepare-volumes +open-api: cd ./open-api && bash ./bin/generate-open-api.sh -open-api-dart: prepare-volumes +open-api-dart: cd ./open-api && bash ./bin/generate-open-api.sh dart -open-api-typescript: prepare-volumes +open-api-typescript: cd ./open-api && bash ./bin/generate-open-api.sh typescript -sql: prepare-volumes +sql: pnpm --filter immich run sync:sql attach-server: @@ -60,20 +60,13 @@ VOLUME_DIRS = \ ./e2e/node_modules \ ./docs/node_modules \ ./server/node_modules \ - ./server/dist \ ./open-api/typescript-sdk/node_modules \ ./.github/node_modules \ ./node_modules \ ./cli/node_modules -# create empty directories and chown to current user -prepare-volumes: - @for dir in $(VOLUME_DIRS); do \ - mkdir -p $$dir; \ - done - @if [ -n "$(VOLUME_DIRS)" ]; then \ - chown -R $$(id -u):$$(id -g) $(VOLUME_DIRS); \ - fi +# Include .env file if it exists +-include docker/.env MODULES = e2e server web cli sdk docs .github @@ -150,8 +143,9 @@ clean: find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' + find . -name "coverage" -type d -prune -exec rm -rf '{}' + find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' + - command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true - command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true + command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true + command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true + setup-server-dev: install-server setup-web-dev: install-sdk build-sdk install-web diff --git a/README.md b/README.md index 459cda481c..0b9b008bca 100644 --- a/README.md +++ b/README.md @@ -38,26 +38,25 @@ ภาษาไทย

-## Disclaimer -- ⚠️ The project is under **very active** development. -- ⚠️ Expect bugs and breaking changes. -- ⚠️ **Do not use the app as the only way to store your photos and videos.** -- ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos! +> [!WARNING] +> ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos! +> + > [!NOTE] > You can find the main documentation, including installation guides, at https://immich.app/. ## Links -- [Documentation](https://immich.app/docs) -- [About](https://immich.app/docs/overview/introduction) -- [Installation](https://immich.app/docs/install/requirements) +- [Documentation](https://docs.immich.app/) +- [About](https://docs.immich.app/overview/introduction) +- [Installation](https://docs.immich.app/install/requirements) - [Roadmap](https://immich.app/roadmap) - [Demo](#demo) - [Features](#features) -- [Translations](https://immich.app/docs/developer/translations) -- [Contributing](https://immich.app/docs/overview/support-the-project) +- [Translations](https://docs.immich.app/developer/translations) +- [Contributing](https://docs.immich.app/overview/support-the-project) ## Demo @@ -106,7 +105,7 @@ Access the demo [here](https://demo.immich.app). For the mobile app, you can use ## Translations -Read more about translations [here](https://immich.app/docs/developer/translations). +Read more about translations [here](https://docs.immich.app/developer/translations). Translation status diff --git a/cli/.nvmrc b/cli/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/cli/README.md b/cli/README.md index 8fa2ace483..ae82e131f4 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,6 +1,6 @@ A command-line interface for interfacing with the self-hosted photo manager [Immich](https://immich.app/). -Please see the [Immich CLI documentation](https://immich.app/docs/features/command-line-interface). +Please see the [Immich CLI documentation](https://docs.immich.app/features/command-line-interface). # For developers diff --git a/cli/package.json b/cli/package.json index 8962ad4645..ce2dab0bd4 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.84", + "version": "2.2.95", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -13,7 +13,6 @@ "cli" ], "devDependencies": { - "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", @@ -21,7 +20,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.17.1", + "@types/node": "^22.18.1", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -69,6 +68,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "22.18.0" + "node": "22.20.0" } } diff --git a/deployment/.env b/deployment/.env new file mode 100644 index 0000000000..f6ce050d29 --- /dev/null +++ b/deployment/.env @@ -0,0 +1,4 @@ +export CLOUDFLARE_ACCOUNT_ID="op://tf/cloudflare/account_id" +export CLOUDFLARE_API_TOKEN="op://tf/cloudflare/api_token" +export TF_STATE_POSTGRES_CONN_STR="op://tf/tf_state/postgres_conn_str" +export TF_VAR_env=$ENVIRONMENT diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl index 90a7bd6259..0869dd28bc 100644 --- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.52.1" - constraints = "4.52.1" + version = "4.52.5" + constraints = "4.52.5" hashes = [ - "h1:2lHvafwGbLdmc9lYkuJFw3nsInaQjRpjX/JfIRKmq/M=", - "h1:596JomwjrtUrOSreq9NNCS+rj70+jOV+0pfja5MXiTI=", - "h1:7mBOA5TVAIt3qAwPXKCtE0RSYeqij9v30mnksuBbpEg=", - "h1:ELVgzh4kHKBCYdL+2A8JjWS0E1snLUN3Mmz3Vo6qSfw=", - "h1:FGGM5yLFf72g3kSXM3LAN64Gf/AkXr5WCmhixgnP+l4=", - "h1:JupkJbQALcIVoMhHImrLeLDsQR1ET7VJLGC7ONxjqGU=", - "h1:KsaE4JNq+1uV1nJsuTcYar/8lyY6zKS5UBEpfYg3wvc=", - "h1:NHZ5RJIzQDLhie/ykl3uI6UPfNQR9Lu5Ti7JPR6X904=", - "h1:NfAuMbn6LQPLDtJhbzO1MX9JMIGLMa8K6CpekvtsuX8=", - "h1:e+vNKokamDsp/kJvFr2pRudzwEz2r49iZ/oSggw+1LY=", - "h1:jnb4VdfNZ79I3yj7Q8x+JmOT+FxbfjjRfrF0dL0yCW8=", - "h1:kmF//O539d7NuHU7qIxDj7Wz4eJmLKFiI5glwQivldU=", - "h1:s6XriaKwOgV4jvKAGPXkrxhhOQxpNU5dceZwi9Z/1k8=", - "h1:wt3WBEBAeSGTlC9OlnTlAALxRiK4SQgLy0KgBIS7qzs=", - "zh:2fb95e1d3229b9b6c704e1a413c7481c60f139780d9641f657b6eb9b633b90f2", - "zh:379c7680983383862236e9e6e720c3114195c40526172188e88d0ffcf50dfe2e", - "zh:55533beb6cfc02d22ffda8cba8027bc2c841bb172cd637ed0d28323d41395f8f", - "zh:5abd70760e4eb1f37a1c307cbd2989ea7c9ba0afb93818c67c1d363a31f75703", - "zh:699f1c8cd66129176fe659ebf0e6337632a8967a28d2630b6ae5948665c0c2ae", - "zh:69c15acd73c451e89de6477059cda2f3ec200b48ae4b9ff3646c4d389fd3205e", - "zh:6e02b687de21b844f8266dff99e93e7c61fc8eb688f4bbb23803caceb251839e", - "zh:7a51d17b87ed87b7bebf2ad9fc7c3a74f16a1b44eee92c779c08eb89258c0496", - "zh:88ad84436837b0f55302f22748505972634e87400d6902260fd6b7ba1610f937", + "h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=", + "h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", + "h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=", + "h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=", + "h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=", + "h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=", + "h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=", + "h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=", + "h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=", + "h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=", + "h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=", + "h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=", + "h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=", + "h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=", + "zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", + "zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", + "zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", + "zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", + "zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", + "zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072", + "zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661", + "zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:8d46c3d9f4f7ad20ac6ef01daa63f4e30a2d16dcb1bb5c7c7ee3dc6be38e9ca1", - "zh:913d64e72a4929dae1d4793e2004f4f9a58b138ea337d9d94fa35cafbf06550a", - "zh:c8d93cf86e2e49f6cec665cfe78b82c144cce15a8b2e30f343385fadd1251849", - "zh:cc4f69397d9bc34a528a5609a024c3a48f54f21616c0008792dd417297add955", - "zh:df99cdb8b064aad35ffea77e645cf6541d0b1b2ebc51b6d26c42031de60ab69e", + "zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", + "zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", + "zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", + "zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", + "zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", + "zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", ] } diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf index 9dd16d5982..63347cf67e 100644 --- a/deployment/modules/cloudflare/docs-release/config.tf +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.52.1" + version = "4.52.5" } } } diff --git a/deployment/modules/cloudflare/docs-release/domain.tf b/deployment/modules/cloudflare/docs-release/domain.tf index 0602045f71..3a6f479a74 100644 --- a/deployment/modules/cloudflare/docs-release/domain.tf +++ b/deployment/modules/cloudflare/docs-release/domain.tf @@ -1,11 +1,11 @@ resource "cloudflare_pages_domain" "immich_app_release_domain" { account_id = var.cloudflare_account_id project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name - domain = "immich.app" + domain = "docs.immich.app" } resource "cloudflare_record" "immich_app_release_domain" { - name = "immich.app" + name = "docs.immich.app" proxied = true ttl = 1 type = "CNAME" diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl index 90a7bd6259..0869dd28bc 100644 --- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.52.1" - constraints = "4.52.1" + version = "4.52.5" + constraints = "4.52.5" hashes = [ - "h1:2lHvafwGbLdmc9lYkuJFw3nsInaQjRpjX/JfIRKmq/M=", - "h1:596JomwjrtUrOSreq9NNCS+rj70+jOV+0pfja5MXiTI=", - "h1:7mBOA5TVAIt3qAwPXKCtE0RSYeqij9v30mnksuBbpEg=", - "h1:ELVgzh4kHKBCYdL+2A8JjWS0E1snLUN3Mmz3Vo6qSfw=", - "h1:FGGM5yLFf72g3kSXM3LAN64Gf/AkXr5WCmhixgnP+l4=", - "h1:JupkJbQALcIVoMhHImrLeLDsQR1ET7VJLGC7ONxjqGU=", - "h1:KsaE4JNq+1uV1nJsuTcYar/8lyY6zKS5UBEpfYg3wvc=", - "h1:NHZ5RJIzQDLhie/ykl3uI6UPfNQR9Lu5Ti7JPR6X904=", - "h1:NfAuMbn6LQPLDtJhbzO1MX9JMIGLMa8K6CpekvtsuX8=", - "h1:e+vNKokamDsp/kJvFr2pRudzwEz2r49iZ/oSggw+1LY=", - "h1:jnb4VdfNZ79I3yj7Q8x+JmOT+FxbfjjRfrF0dL0yCW8=", - "h1:kmF//O539d7NuHU7qIxDj7Wz4eJmLKFiI5glwQivldU=", - "h1:s6XriaKwOgV4jvKAGPXkrxhhOQxpNU5dceZwi9Z/1k8=", - "h1:wt3WBEBAeSGTlC9OlnTlAALxRiK4SQgLy0KgBIS7qzs=", - "zh:2fb95e1d3229b9b6c704e1a413c7481c60f139780d9641f657b6eb9b633b90f2", - "zh:379c7680983383862236e9e6e720c3114195c40526172188e88d0ffcf50dfe2e", - "zh:55533beb6cfc02d22ffda8cba8027bc2c841bb172cd637ed0d28323d41395f8f", - "zh:5abd70760e4eb1f37a1c307cbd2989ea7c9ba0afb93818c67c1d363a31f75703", - "zh:699f1c8cd66129176fe659ebf0e6337632a8967a28d2630b6ae5948665c0c2ae", - "zh:69c15acd73c451e89de6477059cda2f3ec200b48ae4b9ff3646c4d389fd3205e", - "zh:6e02b687de21b844f8266dff99e93e7c61fc8eb688f4bbb23803caceb251839e", - "zh:7a51d17b87ed87b7bebf2ad9fc7c3a74f16a1b44eee92c779c08eb89258c0496", - "zh:88ad84436837b0f55302f22748505972634e87400d6902260fd6b7ba1610f937", + "h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=", + "h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", + "h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=", + "h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=", + "h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=", + "h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=", + "h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=", + "h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=", + "h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=", + "h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=", + "h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=", + "h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=", + "h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=", + "h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=", + "zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", + "zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", + "zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", + "zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", + "zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", + "zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072", + "zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661", + "zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:8d46c3d9f4f7ad20ac6ef01daa63f4e30a2d16dcb1bb5c7c7ee3dc6be38e9ca1", - "zh:913d64e72a4929dae1d4793e2004f4f9a58b138ea337d9d94fa35cafbf06550a", - "zh:c8d93cf86e2e49f6cec665cfe78b82c144cce15a8b2e30f343385fadd1251849", - "zh:cc4f69397d9bc34a528a5609a024c3a48f54f21616c0008792dd417297add955", - "zh:df99cdb8b064aad35ffea77e645cf6541d0b1b2ebc51b6d26c42031de60ab69e", + "zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", + "zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", + "zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", + "zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", + "zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", + "zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", ] } diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf index 9dd16d5982..63347cf67e 100644 --- a/deployment/modules/cloudflare/docs/config.tf +++ b/deployment/modules/cloudflare/docs/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.52.1" + version = "4.52.5" } } } diff --git a/deployment/modules/cloudflare/docs/domain.tf b/deployment/modules/cloudflare/docs/domain.tf index a28fb4c0f8..c5f77de6b4 100644 --- a/deployment/modules/cloudflare/docs/domain.tf +++ b/deployment/modules/cloudflare/docs/domain.tf @@ -1,11 +1,11 @@ resource "cloudflare_pages_domain" "immich_app_branch_domain" { account_id = var.cloudflare_account_id project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name - domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" + domain = "docs.${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" } resource "cloudflare_record" "immich_app_branch_subdomain" { - name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" + name = "docs.${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" proxied = true ttl = 1 type = "CNAME" diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 439140e3f5..eba1f632c1 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,5 +1,5 @@ # -# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose # # Make sure to use the docker-compose.yml of the current release: # @@ -8,8 +8,8 @@ # The compose file on main may not be compatible with the latest release. # For development see: -# - https://immich.app/docs/developer/setup -# - https://immich.app/docs/developer/troubleshooting +# - https://docs.immich.app/developer/setup +# - https://docs.immich.app/developer/troubleshooting name: immich-dev @@ -21,16 +21,14 @@ services: # extends: # file: hwaccel.transcoding.yml # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding - user: '${UID:-1000}:${GID:-1000}' build: context: ../ - dockerfile: server/Dockerfile + dockerfile: server/Dockerfile.dev target: dev restart: unless-stopped volumes: - ..:/usr/src/app - ${UPLOAD_LOCATION}/photos:/data - - ${UPLOAD_LOCATION}/photos/upload:/data/upload - /etc/localtime:/etc/localtime:ro - pnpm-store:/usr/src/app/.pnpm-store - server-node_modules:/usr/src/app/server/node_modules @@ -57,8 +55,8 @@ services: IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server 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/community-guides + IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app + IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides ulimits: nofile: soft: 1048576 @@ -72,20 +70,15 @@ services: condition: service_started database: condition: service_started - init: - condition: service_completed_successfully healthcheck: disable: false immich-web: container_name: immich_web image: immich-web-dev:latest - # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919 - # user: 0:0 - user: '${UID:-1000}:${GID:-1000}' build: context: ../ - dockerfile: server/Dockerfile + dockerfile: server/Dockerfile.dev target: dev command: ['immich-web'] env_file: @@ -114,8 +107,6 @@ services: depends_on: immich-server: condition: service_started - init: - condition: service_completed_successfully immich-machine-learning: container_name: immich_machine_learning @@ -143,13 +134,13 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280 + image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 healthcheck: test: redis-cli ping || exit 1 database: container_name: immich_postgres - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84 env_file: - .env environment: @@ -183,25 +174,6 @@ services: # volumes: # - grafana-data:/var/lib/grafana - init: - container_name: init - image: busybox - env_file: - - .env - user: 0:0 - command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' - volumes: - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage volumes: model-cache: prometheus-data: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 7c658de336..e27012cf56 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -1,5 +1,5 @@ # -# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose # # Make sure to use the docker-compose.yml of the current release: # @@ -56,14 +56,14 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280 + image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 healthcheck: test: redis-cli ping || exit 1 restart: always database: container_name: immich_postgres - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84 env_file: - .env environment: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 052ae8b334..b4ff05f366 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,5 @@ # -# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose # # Make sure to use the docker-compose.yml of the current release: # @@ -36,7 +36,7 @@ services: # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag. # Example tag: ${IMMICH_VERSION:-release}-cuda image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} - # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration + # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration # file: hwaccel.ml.yml # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable volumes: @@ -49,14 +49,14 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280 + image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 healthcheck: test: redis-cli ping || exit 1 restart: always database: container_name: immich_postgres - image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} diff --git a/docker/example.env b/docker/example.env index 0450dc0805..6d6fd1e3fe 100644 --- a/docker/example.env +++ b/docker/example.env @@ -1,4 +1,4 @@ -# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables +# You can find documentation for all the supported env variables at https://docs.immich.app/install/environment-variables # The location where your uploaded files are stored UPLOAD_LOCATION=./library diff --git a/docker/hwaccel.ml.yml b/docker/hwaccel.ml.yml index 111202d022..c95ac7ee4c 100644 --- a/docker/hwaccel.ml.yml +++ b/docker/hwaccel.ml.yml @@ -4,7 +4,7 @@ # you can inline the config for a backend by copying its contents # into the immich-machine-learning service in the docker-compose.yml file. -# See https://immich.app/docs/features/ml-hardware-acceleration for info on usage. +# See https://docs.immich.app/features/ml-hardware-acceleration for info on usage. services: armnn: diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml index 60ee7e8fa3..0857faf465 100644 --- a/docker/hwaccel.transcoding.yml +++ b/docker/hwaccel.transcoding.yml @@ -4,7 +4,7 @@ # you can inline the config for a backend by copying its contents # into the immich-microservices service in the docker-compose.yml file. -# See https://immich.app/docs/features/hardware-transcoding for more info on using hardware transcoding. +# See https://docs.immich.app/features/hardware-transcoding for more info on using hardware transcoding. services: cpu: {} diff --git a/docs/.nvmrc b/docs/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/docs/blog/2022/11-10/release-1.36.mdx b/docs/blog/2022/11-10/release-1.36.mdx deleted file mode 100644 index 5f5643196c..0000000000 --- a/docs/blog/2022/11-10/release-1.36.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -slug: release-1-36 -title: Release v1.36.0 -authors: [alextran] -tags: [release] -date: 2022-11-10 ---- - -Hello everyone, it is my pleasure to deliver the new release of Immich to you. The team has been working hard to bring you the new features and improvements. This release includes some big features that the community has been asking since the beginning of Immich. We hope you will enjoy it. - -Some notable features are: - -- OAuth integration -- LivePhoto support on iOS -- User config system - - - -## LivePhoto iOS Support 🎉 - -LivePhoto on iOS is now supported in Immich. - -The motion part will now be uploaded and can be played on the mobile app and the web. - -:::caution - -- The server and the app has to be on version **1.36.x** for the application to work correctly. -- Previous uploaded photos will not be updated automatically, you will have to remove and reupload them if you want to keep the LivePhoto functionality. - -::: - - - -## OAuth Integration 🎉 - -I want to borrow this chance to express my gratitude to [@EnricoBilla](https://github.com/EnricoBilla), who has been the trailblazer for this feature since the beginning days of Immich. His PR has sparked ideas, suggestions, and discussion among the team member on how to integrate this feature successfully into the app. Thank you so much for your work and your time. - -OAuth is now integrated into the system. Please follow the guide [here](https://immich.app/docs/usage/oauth) to set up your OAuth integration - -After setting up the correct environment variables in the `.env` file, as shown below - -| Key | Type | Default | Description | -| ------------------- | ------- | -------------------- | ------------------------------------------------------------------------- | -| OAUTH_ENABLED | boolean | false | Enable/disable OAuth2 | -| OAUTH_ISSUER_URL | URL | (required) | Required. Self-discovery URL for client | -| OAUTH_CLIENT_ID | string | (required) | Required. Client ID | -| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret | -| OAUTH_SCOPE | string | openid email profile | Full list of scopes to send with the request (space delimited) | -| OAUTH_AUTO_REGISTER | boolean | true | When true, will automatically register a user the first time they sign in | -| OAUTH_BUTTON_TEXT | string | Login with OAuth | Text for the OAuth button on the web | - -```bash title="Authentik Example" -OAUTH_ENABLED=true -OAUTH_ISSUER_URL=http://10.1.15.216:9000/application/o/immich-test/ -OAUTH_CLIENT_ID=30596v8f78a4b6a97d5985c3076b6b4c4d12ddc33 -OAUTH_CLIENT_SECRET=50f1eafdec353b95b1c638db390db4ab67ef035a51212dbec2f56175e2eb272b5d572c099176e6fe116ecf47ffdd544bgdb9e2edc588307ee0339d25eeccd88 -OAUTH_BUTTON_TEXT=Login with Authentik -``` - -The web will have the option to sign in with OAuth. - - - -The mobile app will check if the server has OAuth enabled before displaying the OAuth -sign-in button. - - - -## Support - - - -If you find the project helpful and it helps you in some ways, you can support the project [one time](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or [monthly](https://github.com/sponsors/alextran1502) from GitHub Sponsor - -It is a great way to let me know that you want me to continue developing and working on this project for years to come. - -## Details - -For more details, please check out the [release note](https://github.com/immich-app/immich/releases/tag/v1.36.0_55-dev) diff --git a/docs/blog/2023/06-24/update.mdx b/docs/blog/2023/06-24/update.mdx deleted file mode 100644 index 464d3e44d9..0000000000 --- a/docs/blog/2023/06-24/update.mdx +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Immich Update - June 2023 -authors: [alextran] -tags: [update] ---- - -Hello everybody, Alex here! - -I am back with another update on Immich. It has been only a month since my last update (May 18th, 2023), but it seems forever. I think the rapid releases of Immich and the amount of work make the perspective of time change in Immich’s world. We have some exciting updates that I think you will like. - -Before going into detail, on behalf of the core team, I would like to thank all of you for loving Immich and contributing to the project. Thank you for helping me make Immich an enjoyable alternative solution to Google Photos so that you have complete control of your data and privacy. I know we are still young and have a lot of work to do, but I am confident we will get there with help from the community. I appreciate all of you from the bottom of my heart! - - - -And now, to the exciting part, what is new in Immich’s world? - -- Initial support for existing gallery. -- Memory feature. -- Support XMP sidecar. -- Support more raw formats. -- Justified layout for web timeline and blurred thumbnail hash. -- Mechanism to host machine learning on a completely different machine. - -## Support for existing gallery - -I know this is the most controversial feature when it comes to Immich’s way of ingesting photos and videos. For many users, having to upload photos and videos to Immich is simply not working. We listen, discuss, and digest this feature internally more than you imagine because it is not a simple feature to tackle while keeping the performance and the user experience at the top level, which is Immich’s primary goal. - -Thankfully, we have many great contributors and developers that want to make this come true. So we came up with an initial implementation of this feature in the form of a supporting read-only gallery. - -To be concise, Immich can now read in the gallery files, register the path into the database, and then generate necessary files and put them through Immich’s machine learning pipeline so you can use all the goodness of Immich without the need to upload them. Since this is the initial implementation, some actions/behavior are not yet supported, and we aim to build toward them in future releases, namely: - -- Assets are not automatically synced and must instead be manually synced with the CLI tool. -- Only new files that are added to the gallery will be detected. -- Deleted and moved files will not be detected. - -## Memory feature - -This is considered a fun feature that the team and I wanted to build for so long, but we had to put it off because of the refactoring of the code base. The code base is now in a good enough form to circle back and add more exciting features. - -This memory feature is very much similar to GPhotos' implementation of “x years since…”. We are aiming to add more categories of memories in the future, such as “Spotlight of the day” or “Day of the Week highlights” - - - -This feature is now available on the web and will be ported to the mobile app in the near future. - -## Support XMP Sidecar - -Immich can now import/upload XMP sidecars from the CLI and use the information as the metadata of assets. - -## Support more raw formats. - -With the recent updates on the dependencies of Immich, we are now extending and hardening support for multiple raw formats. So users with DSLR or mirrorless cameras can now upload their original files to Immich and have them displayed in high-quality thumbnails on the web and mobile view. - -## Justified layout for web timeline and blurred thumbnail hash - -This is an aesthetic improvement in user experience when browsing the timeline. Photos and videos are now displayed correctly with perspective orientation, making the browsing experience more pleasurable. - -To further improve the browsing experience, we now added a blur hash to the thumbnail, so the transition is more natural with a dreamy fade in effect, similar to how our brain goes from faded to vivid memory - - - -## Hosting machine learning container on a different machine - -With more capabilities Immich is building toward, machine learning will get more powerful and therefore require more resources to run effectively. However, we understand that users might not have the best server resources where they host the Immich instance. Therefore, we changed how machine learning interacts and receives the photos and videos to run through its inference pipeline. - -The machine learning container is now a headless system that can run on any machine. As long as your Immich instance can communicate with the system running the machine learning container, it can send the files and receive the required information to make Immich powerful in terms of searching and intelligence. This helps you to utilize a more powerful machine in your home/infrastructure to perform the CPU-intensive tasks while letting Immich only handle the I/O operations for a pleasant and smooth experience. - ---- - -So, those are the highlights for the team and the community after a busy month. There are a lot more changes and improvements. I encourage you to read some release notes, starting from version [v1.57.0](https://github.com/immich-app/immich/releases/tag/v1.57.0) to now. - -Thank you, and I am asking for your support for the project. I hope to be a full-time maintainer of Immich one day to dedicate myself to the project as my life works for the community and my family. You can find the support channels below: - -- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502) -- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) -- [Liberapay](https://liberapay.com/alex.tran1502/) -- [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 -- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky. - -Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything - -Cheer! - -Until next time! - -Alex diff --git a/docs/blog/2023/07-29/images/web-shortcuts-panel.png b/docs/blog/2023/07-29/images/web-shortcuts-panel.png deleted file mode 100644 index 5a16c9f289..0000000000 Binary files a/docs/blog/2023/07-29/images/web-shortcuts-panel.png and /dev/null differ diff --git a/docs/blog/2023/07-29/update.mdx b/docs/blog/2023/07-29/update.mdx deleted file mode 100644 index 6d50ddfdc0..0000000000 --- a/docs/blog/2023/07-29/update.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Immich Update - July 2023 -authors: [alextran] -tags: [update, v1.64.0-v1.71.0] ---- - -Hello, Immich fans, another month, another milestone. We hope you are staying cool and safe in this scorching hot summer across the globe. - -Immich recently got some good recognition when getting to the front page of HackerNews, which helped to let more people know about the project's existence. The project will help more and more people find a solution to control the privacy of their most precious moments. And with the gain in popularity and recognition, we have gotten new users and more questions from the community than ever. - -I want to express my gratitude to all the contributors and the community who have been tremendously helpful to new users' questions and provided technical support. - -Below are the highlights of new features we added to the application over the past month, along with countless bug fixes and improvements across the board, from developer experience to resource optimization and UI/UX improvement. I hope you find these topics as exciting as I am. - -## Highlights - -- Memories feature. -- Facial recognition improvements. -- Improvements on multi selection behavior on the web. -- Shortcuts for common actions on the web. -- Support viewer for 360-panorama photos. - - - ---- - -### Memories feature - -We've added the memory feature on the mobile app, so you can reminisce about your past memories. - - - -### Facial recognition improvements - -Over the past few releases, we have added many UI improvements to the facial recognition feature to help you manage the recognized people better. Some of the highlights: - -#### Choose a new feature photo for a person. - - - -#### Hide and show faces. - -You can now select irrelevant faces to hide them. The hidden faces won’t be displayed in search results and the people section in the info panel. - -#### Merge faces. - -This is useful when you have multiple faces of the same person in your photos, and you want to merge them into one. - - - -We also added a nifty mechanism that when naming a face, similar names will prompt you a merge face option for the convenience. - - - -### Improvements on multi selection behavior on the web - -We have added a new multi selection behavior on the web to help you select multiple items easier. You can now select a range of photos and videos by holding the `Shift` key. - - - -### Shortcuts for common actions on the web. - -Some of us only navigate the world and the web with a keyboard (looking at you, Vim and Emacs users). So it would take away the sacred weapon of choice to require many clicks to perform repetitive actions. So we added quick shortcuts for the following action on the web. - -Dot Env Example - -### Support viewer for 360-panorama photos. - -Photos with the EXIF property of `ProjectionType` will now have a special viewer on the web to view all the angles of the panorama. - -The thumbnail of the 360 degrees panoramas will have a special icon on the top right of the thumbnail - -Dot Env Example - -Panorama in the detail view - -Dot Env Example - ---- - -Thank you, and I am asking for your support for the project. I hope to be a full-time maintainer of Immich one day to dedicate myself to the project as my life's work for the community and my family. You can find the support channels below: - -- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502) -- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) -- [Liberapay](https://liberapay.com/alex.tran1502/) -- [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 -- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky. - -Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything - -Cheer! - -Until next time! - -Alex diff --git a/docs/blog/2023/2023-recap.mdx b/docs/blog/2023/2023-recap.mdx deleted file mode 100644 index e9d93a52be..0000000000 --- a/docs/blog/2023/2023-recap.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Immich Recap 2023 -authors: [alextran] -tags: [update, recap-2023] -date: 2023-12-30T00:00 ---- - -Hi everyone, - -Alex from Immich here. - -We are entering the last few weeks of 2023, and it has been quite a year for Immich. The project has grown so much in terms of users, developers, features, maturity, and the community around it. When I started working on Immich, it was simply a challenge for myself and an opportunity to learn new technologies, crafting something fun and useful for my wife during my free time to satisfy my urge to build and create things. I never thought it would become so popular and help so many people. At the end of the day, all we have is memory. I am proud that the team and I have created something to make storing and viewing those precious memories easier without restrictions and without sacrificing our privacy. As the year closes, here’s a recap of everything the project accomplished in 2023. - -# Milestones - -- Public shared links -- Favorites page -- Immich turned 1 -- Material Design 3 on the mobile app -- Auto-link LivePhotos server-side -- iOS background backup -- Explore page -- CLIP search -- Search by metadata -- Responsive web app -- Archive page -- Asset descriptions -- 10,000 stars on GitHub -- Manage auth devices -- Map view -- Facial recognition, clustering, searching, renaming, and person management -- Partner sharing and unifying timeline between partners' users -- Custom storage label -- XMP sidecar reading -- RAW file formats -- Justified layout on the web -- Memories -- Multi-select via SHIFT -- Android Motion Photos -- 360° Photos -- Album description -- Album performance improvements (time buckets) -- Video hardware transcoding -- Slideshow mode on the web -- Configuration file -- External libraries -- Trash page -- Custom theme -- Asset Stacking -- 20,000 stars on GitHub -- Shared album activity and comments -- CLI v2 -- Down to 5 containers (from 8) - -# Fun Statistics - -- We have gone from the release version `1.41.0` to `1.90.0` at the time of writing. On average, we see a release every 7 days. -- According to GitHub's metrics, the `immich-server` container image has been pulled almost _4 million_ times. -- According to mobile app store metrics, we have 22,000 installations on Android and 6700 installation units on iOS (opt-in only). -- Immich is making around $1200/month on average from donations. (Thank you all so much!) -- We were guests on two podcasts: - - [Self-hosted](https://selfhosted.show/110) - - [The Vergecast](https://www.theverge.com/23938533/self-hosting-local-first-software-vergecast) -- There are over 4,500 members on the Discord server. -- We have over 22,000 stars on the main GitHub repository, gaining 15,000 stars since January 2023. - -Diving into the next year, the team will continue to build on the foundation we have laid out over the past year, implementing more advanced features for searching, organizing, and sharing between users. Bugs will continue to be squashed and conquered. “Shit Alex wrote'' code will continue to be replaced by beautiful, clean code from Jason, Zack, Boet, Daniel, Osorin, Mert, Fynn, Marty, Martin, and Jonathan. The team has my eternal gratitude for creating a welcoming environment for new contributors, helping, teaching, and learning from each other. I’ve realized that hardly a day has gone by where the team hasn’t been in communication about Immich related topics over the past year. - -My long-term goal is to help hone Immich into a diamond in the FOSS space, where the UI, UX, development experiences, documentation, and quality are at a high standard while remaining free for everybody to use. - -I hope you enjoy Immich and have a happy and peaceful holiday. diff --git a/docs/blog/2024/immich-core-team-goes-fulltime.mdx b/docs/blog/2024/immich-core-team-goes-fulltime.mdx deleted file mode 100644 index 0cba2b467c..0000000000 --- a/docs/blog/2024/immich-core-team-goes-fulltime.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: The Immich core team goes full-time -authors: [alextran] -tags: [update, announcement, FUTO] -date: 2024-05-01T00:00 ---- - -**Immich is joining [FUTO](https://futo.org/)!** - -Since the beginning of this adventure, my goal has always been to create a better world for my children. Memories are priceless, and privacy should not be a luxury. However, building quality open source has its challenges. Over the past two years, it has taken significant dedication, time, and effort. - -Recently, a company in Austin, Texas, called FUTO contacted the team. FUTO strives to develop quality and sustainable open software. They build software alternatives that focus on giving control to users. From their mission statement: - -“Computers should belong to you, the people. We develop and fund technology to give them back.” - -FUTO loved Immich and wanted to see if we’d consider working with them to take the project to the next level. In short, FUTO offered to: - -- Pay the core team to work on Immich full-time -- Let us keep full autonomy about the project’s direction and leadership -- Continue to license Immich under AGPL -- Keep Immich’s development direction with no paywalled features -- Keep Immich “built for the people” (no ads, data mining/selling, or alternative motives) -- Provide us with financial, technical, legal, and administrative support - -After careful deliberation, the team decided that FUTO’s vision closely aligns with our own: to build a better future by providing a polished, performant, and privacy-preserving open-source software solution for photo and video management delivered in a sustainable way. - -Immich’s future has never looked brighter, and we look forward to realizing our vision for Immich as part of FUTO. - -If you have more questions, we’ll host a Q&A live stream on May 9th at 3PM UTC (10AM CST). [You can ask questions here](https://www.live-ask.com/event/01HWP2SB99A1K8EXFBDKZ5Z9CF), and the stream will be live [here on our YouTube channel](https://youtube.com/live/cwz2iZwYpgg). - -Cheers, - -The Immich Team - ---- - -## FAQs - -### What is FUTO? - -[https://futo.org/what-is-futo/](https://futo.org/what-is-futo/) - -### Will the license change? - -No. Immich will continue to be licensed under AGPL without a CLA. - -### Will Immich continue to be free? - -Yes. The Immich source code will remain freely available under the AGPL license. - -### Is Immich getting VC funding? - -No. Venture capital implies investment in a business, often with the expectation of a future payout (exit plan). Immich is neither a business that can be acquired nor comes with a money-making exit plan. - -### I am currently supporting Immich through GitHub sponsors. What will happen to my donation? - -Effective immediately, all donations to the Immich organization will be canceled. In the future, we will offer an optional, modest payment option instead. Thank you to everyone who donated to help us get this far! - -### How is funding sustainable? - -Immich and FUTO believe a sustainable future requires a model that does not rely on users-as-a-product. To this end, FUTO advocates that users pay for good, open software. In keeping with this model, we will adopt a purchase price. This means we no longer accept donations, but — _without limiting features for those who do not pay_ — we will soon allow you to purchase Immich through a modest payment. We encourage you to pay for the high-quality software you use to foster a healthy software culture where developers build great applications without hidden motives for their users. - -### When does this change take effect? - -This change takes effect immediately. - -### What will change? - -The following things will change as Immich joins FUTO: - -- The brand, logo, and other Immich trademarks will be transferred to FUTO. -- We will stop all donations to the project. -- The core team can now dedicate our full attention to Immich -- Before the end of the year, we plan to have a roadmap for what it will take to get Immich to a stable release. -- Bugs will be squashed, and features will be delivered faster. diff --git a/docs/blog/2024/immich-licensing.mdx b/docs/blog/2024/immich-licensing.mdx deleted file mode 100644 index 773abcb666..0000000000 --- a/docs/blog/2024/immich-licensing.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Licensing announcement - Purchase a license to support Immich -authors: [alextran] -tags: [update, announcement, FUTO] -date: 2024-07-18T00:00 ---- - -Hello everybody, - -Firstly, on behalf of the Immich team, I'd like to thank everybody for your continuous support of Immich since the very first day! Your contributions, encouragement, and community engagement have helped bring Immich to its current state. The team and I are forever grateful for that. - -Since our [last announcement of the core team joining FUTO to work on Immich full-time](https://immich.app/blog/2024/immich-core-team-goes-fulltime), one of the goals of our new position is to foster a healthy relationship between the developers and the users. We believe that this enables us to create great software, establish transparent policies and build trust. - -We want to build a great software application that brings value to you and your loved ones' lives. We are not using you as a product, i.e., selling or tracking your data. We are not putting annoying ads into our software. We respect your privacy. We want to be compensated for the hard work we put in to build Immich for you. - -With those notes, we have enabled a way for you to financially support the continued development of Immich, ensuring the software can move forward and will be maintained, by offering a lifetime license of the software. We think if you like and use software, you should pay for it, but _we're never going to force anyone to pay or try to limit Immich for those who don't._ - -There are two types of license that you can choose to purchase: **Server License** and **Individual License**. - -### Server License - -This is a lifetime license costing **$99.99**. The license is applied to the whole server. You and all users that use your server are licensed. - -### Individual License - -This is a lifetime license costing **$24.99**. The license is applied to a single user, and can be used on any server they choose to connect to. - -license-social-gh - -You can purchase the license on [our page - https://buy.immich.app](https://buy.immich.app). - -Starting with release `v1.109.0` you can purchase and enter your purchased license key directly in the app. - -license-page-gh - -## Thank you - -Thank you again for your support, this will help create a strong foundation and stability for the Immich team to continue developing and maintaining the project that you love to use. - -

- -

- -
-
- -Cheers! 🎉 - -Immich team - -# FAQ - -### 1. Where can I purchase a license? - -There are several places where you can purchase the license from - -- [https://buy.immich.app](https://buy.immich.app) -- [https://pay.futo.org](https://pay.futo.org/) -- or directly from the app. - -### 2. Do I need both _Individual License_ and _Server License_? - -No, - -If you are the admin and the sole user, or your instance has less than a total of 4 users, you can buy the **Individual License** for each user. - -If your instance has more than 4 users, it is more cost-effective to buy the **Server License**, which will license all the users on your instance. - -### 3. What do I do if I don't pay? - -You can continue using Immich without any restriction. - -### 4. Will there be any paywalled features? - -No, there will never be any paywalled features. - -### 5. Where can I get support regarding payment issues? - -You can email us with your `orderId` and your email address `billing@futo.org` or on our Discord server. diff --git a/docs/blog/2024/update-july-2024.mdx b/docs/blog/2024/update-july-2024.mdx deleted file mode 100644 index cbe99177e7..0000000000 --- a/docs/blog/2024/update-july-2024.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Immich Update - July 2024 -authors: [alextran] -date: 2024-07-01T00:00 -tags: [update, v1.106.0] ---- - -Hello everybody! Alex from Immich here and I am back with another development progress update for the project. - -Summer has returned once again, and the night sky is filled with stars, thank you for **38_000 shining stars** you have sent to our [GitHub repo](https://github.com/immich-app/immich)! Since the last announcement several core contributors have started full time. Everything is going great with development, PRs get merged with _brrrrrrr_ rate, conversation exchange between team members is on a new high, we met and are working with the great engineers at FUTO. The spirit is high and we have a lot of things brewing that we think you will like. - -Let's go over some of the updates we had since the last post. - -### Container consolidation - -Reduced the number of total containers from 5 to 4 by making the microservices thread get spawned directly in the server container. Woohoo, remember when Immich had 7 containers? - -### Email notifications - -![smtp](https://github.com/immich-app/immich/assets/27055614/949cba85-d3f1-4cd3-b246-a6f5fb5d3ae8) - -We added email notifications to the app with SMTP settings that you can configure for the following events - -- A new account is created for you. -- You are added to a shared album. -- New media is added to an album. - -### Versioned docs - -You can now jump back into the past or take a peek at the unreleased version of the documentation by selecting the version on the website. - -![version-doc](https://github.com/immich-app/immich/assets/27055614/6d22898a-5093-41ad-b416-4573d7ce6e03) - -### Similarity deduplication - -With more machine learning and CLIP magic, we now have similarity deduplication built into the application where it will search for closely similar images and let you decide what to do with them; i.e keep or trash. - -![similarity-deduplication](https://github.com/immich-app/immich/assets/27055614/3cac8478-fbf7-47ea-acb6-0146901dc67e) - -### Permanent URL for asset on the web - -The detail view for an asset now has a permanent URL so you can easily share them with your loved ones. - -### Web app translations - -We now have a public Weblate project which the community can use to translate the webapp to their native languages. We are planning to port the mobile app translation to this platform as well. If you would like to contribute, you can take a look [here](https://hosted.weblate.org/projects/immich/immich/). We're already close to 50% translations -- we really appreciate everyone contributing to that! - -![web-translation](https://github.com/immich-app/immich/assets/27055614/363df2ed-656c-4584-bd82-0708a693c5bc) - -### Read-only/Editor mode on shared album - -As the owner of the album, you can choose if the shared user can edit the album or to only view the content of the album without any modification. - -![read-only-album](https://github.com/immich-app/immich/assets/27055614/c6f66375-b869-495a-9a86-3e87b316d109) - -### Better video thumbnails - -Immich now tries to find a descriptive video thumbnail instead of simply using the first frame. No more black images for thumbnails! - -### Public Roadmap - -We now have a [public roadmap](https://immich.app/roadmap), giving you a high-level overview of things the team is working on. The first goal of this roadmap is to bring Immich to a stable release, which is expected sometime later this year. Some of the highlights include - -- Auto stacking - Auto stacking of burst photos -- Basic editor - Basic photo editing capabilities -- Workflows - Automate tasks with workflows -- Fine grained access controls - Granular access controls for users and api keys -- Better background backups - Rework background backups to be more reliable -- Private/locked photos - Private assets with extra protections - -Beyond the items in the roadmap, we have _many many_ more ideas for Immich. The team and I hope that you are enjoying the application, find it helpful in your life and we have nothing but the intention of building out great software for you all! - -Have an amazing Summer or Winter for those in the southern hemisphere! :D - -Until next time, - -Cheers! -Alex diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml deleted file mode 100644 index f331efa927..0000000000 --- a/docs/blog/authors.yml +++ /dev/null @@ -1,5 +0,0 @@ -alextran: - name: Alex Tran - title: Maintainer of Immich - url: https://github.com/alextran1502 - image_url: https://github.com/alextran1502.png diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index e57039a420..14ac1de298 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -1,14 +1,40 @@ # FAQ +## Commercial Guidelines + +### Are you open to commercial partnerships and collaborations? + +We are working to commercialize Immich and we'd love for you to help us by making Immich better. FUTO is dedicated to developing sustainable models for developing open source software for our customers. We want our customers to be delighted by the products our engineers deliver, and we want our engineers to be paid when they succeed. + +If you wish to use Immich in a commercial product not owned by FUTO, we have the following requirements: + +- Plugin Integrations: Integrations for other platforms are typically approved, provided proper notification is given. + +- Reseller Partnerships: Must adhere to the guidelines outlined below regarding trademark usage, and proper representation. + +- Strategic Collaborations: We welcome discussions about mutually beneficial partnerships that enhance the value proposition for both organizations. + +### What are your guidelines for resellers and trademark usage? + +For organizations seeking to resell Immich, we have established the following guidelines to protect our brand integrity and ensure proper representation. + +- We request that resellers do not display our trademarks on their websites or marketing materials. If such usage is discovered, we will contact you to request removal. + +- Do not misrepresent your reseller site or services as being officially affiliated with or endorsed by Immich or our development team. + +- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directy from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work. + +When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app + ## User ### How can I reset the admin password? -The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server. +The admin password can be reset by running the [reset-admin-password](/administration/server-commands.md) command on the immich-server. ### How can I see a list of all users in Immich? -You can see the list of all users by running [list-users](/docs/administration/server-commands.md) Command on the Immich-server. +You can see the list of all users by running [list-users](/administration/server-commands.md) Command on the Immich-server. --- @@ -80,20 +106,20 @@ However, Immich will delete original files that have been trashed when the trash When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job. -It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation. +It is recommended to read about [Storage Template](/administration/storage-template) before activation. ### Can I add my existing photo library? -Yes, with an [External Library](/docs/features/libraries.md). +Yes, with an [External Library](/features/libraries.md). -### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)? +### What happens to existing files after I choose a new [Storage Template](/administration/storage-template.mdx)? -Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs-workers/#jobs) page. +Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/administration/jobs-workers/#jobs) page. ### Why are only photos and not videos being uploaded to Immich? This often happens when using a reverse proxy in front of Immich. -Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to allow large requests. +Make sure to [set your reverse proxy](/administration/reverse-proxy/) to allow large requests. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails. @@ -113,7 +139,7 @@ You can _archive_ them. ### How can I backup data from Immich? -See [Backup and Restore](/docs/administration/backup-and-restore.md). +See [Backup and Restore](/administration/backup-and-restore.md). ### Does Immich support reading existing face tag metadata? @@ -199,7 +225,7 @@ volumes: ### Can I keep my existing album structure while importing assets into Immich? -Yes, by using the [Immich CLI](/docs/features/command-line-interface) along with the `--album` flag. +Yes, by using the [Immich CLI](/features/command-line-interface) along with the `--album` flag. ### Is there a way to reorder photos within an album? @@ -240,7 +266,7 @@ Immich uses CLIP models. An ML model converts each image to an "embedding", whic ### How does facial recognition work? -See [How Facial Recognition Works](/docs/features/facial-recognition#how-facial-recognition-works) for details. +See [How Facial Recognition Works](/features/facial-recognition#how-facial-recognition-works) for details. ### How can I disable machine learning? @@ -262,7 +288,7 @@ No, this is not supported. Only models listed in the [Hugging Face][huggingface] ### I want to be able to search in other languages besides English. How can I do that? -You can change to a multilingual CLIP model. See [here](/docs/features/searching#clip-models) for instructions. +You can change to a multilingual CLIP model. See [here](/features/searching#clip-models) for instructions. ### Does Immich support Facial Recognition for videos? @@ -273,7 +299,7 @@ Scanning the entire video for faces may be implemented in the future. No. :::tip -You can use [Smart Search](/docs/features/searching.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results. +You can use [Smart Search](/features/searching.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results. ::: ### I'm getting a lot of "faces" that aren't faces, what can I do? @@ -303,7 +329,7 @@ ls clip/ facial-recognition/ ### Why is Immich slow on low-memory systems like the Raspberry Pi? -Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely. +Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/guides/remote-machine-learning), or [disable](/FAQ#how-can-i-disable-machine-learning) machine learning entirely. ### Can I lower CPU and RAM usage? @@ -313,9 +339,9 @@ The initial backup is the most intensive due to the number of jobs running. The - Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2. - Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good. - For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this. -- At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further. +- At the container level, you can [set resource constraints](/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further. - It's recommended to only apply these constraints _after_ taking some of the measures here for best performance. -- If these changes are not enough, see [above](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning. +- If these changes are not enough, see [above](/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning. ### Can I limit CPU and RAM usage? @@ -357,7 +383,7 @@ Do not exaggerate with the job concurrency because you're probably thoroughly ov ### My server shows Server Status Offline | Version Unknown. What can I do? -You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your reverse proxy. +You need to [enable WebSockets](/administration/reverse-proxy/) on your reverse proxy. --- @@ -365,7 +391,7 @@ You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your rev ### How can I see Immich logs? -Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md). +Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/guides/docker-help.md). ### How can I reduce the log verbosity of Redis? @@ -409,7 +435,7 @@ cap_drop: Data for Immich comes in two forms: 1. **Metadata** stored in a Postgres database, stored in the `DB_DATA_LOCATION` folder (previously `pg_data` Docker volume). -2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations). +2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/administration/backup-and-restore#asset-types-and-storage-locations). :::warning This will destroy your database and reset your instance, meaning that you start from scratch. @@ -447,7 +473,7 @@ If it mentions SIGILL (note the lack of a K) or error code 132, it most likely m ### Why am I getting database ownership errors? If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem. -NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/requirements#special-requirements-for-windows-users) for more details. +NTFS and ex/FAT/32 filesystems are not supported. See [here](/install/requirements#special-requirements-for-windows-users) for more details. ### How can I verify the integrity of my database? diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index deeefa5635..f9c00c7df7 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -3,7 +3,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/docs/guides/template-backup-script.md) +A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/guides/template-backup-script.md) :::danger The instructions on this page show you how to prepare your Immich instance to be backed up, and which files to take a backup of. You still need to take care of using an actual backup tool to make a backup yourself. @@ -160,7 +160,7 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele :::danger A backup of this folder does not constitute a backup of your database! - Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup. + Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup. ::: @@ -205,7 +205,7 @@ When you turn off the storage template engine, it will leave the assets in `UPLO :::danger A backup of this folder does not constitute a backup of your database! - Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup. + Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup. ::: diff --git a/docs/docs/administration/email-notification.mdx b/docs/docs/administration/email-notification.mdx index 2ad4fba2be..0da132161f 100644 --- a/docs/docs/administration/email-notification.mdx +++ b/docs/docs/administration/email-notification.mdx @@ -12,7 +12,7 @@ You can access the settings panel from the web at `Administration -> Settings -> Under Email, enter the required details to connect with an SMTP server. -You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server. +You can use [this guide](/guides/smtp-gmail) to use Gmail's SMTP server. ## User's notifications settings diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md index 4634151b9a..75f50599a0 100644 --- a/docs/docs/administration/jobs-workers.md +++ b/docs/docs/administration/jobs-workers.md @@ -11,7 +11,7 @@ The `immich-server` container contains multiple workers: ## Split workers -If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/docs/install/environment-variables) to specify which container should pick up which tasks. +If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/install/environment-variables) to specify which container should pick up which tasks. For example, for a simple setup with one container for the Web/API and one for all other microservices, you can do the following: @@ -53,5 +53,5 @@ Additionally, some jobs (such as memories generation) run on a schedule, which i :::note -Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings. +Some jobs ([External Libraries](/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings. ::: diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 55a0ce9469..47f4a96c6a 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -28,7 +28,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured 2. Configure Redirect URIs/Origins The **Sign-in redirect URIs** should include: - - `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx) + - `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/features/mobile-app.mdx) - `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client - `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client @@ -98,7 +98,7 @@ The redirect URI for the mobile app is `app.immich:///oauth-callback`, which is 2. Whitelist the new endpoint as a valid redirect URI with your provider. 3. Specify the new endpoint as the `Mobile Redirect URI Override`, in the OAuth settings. -With these steps in place, you should be able to use OAuth from the [Mobile App](/docs/features/mobile-app.mdx) without a custom scheme redirect URI. +With these steps in place, you should be able to use OAuth from the [Mobile App](/features/mobile-app.mdx) without a custom scheme redirect URI. :::info Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:///oauth-callback`, and can be used for step 1. diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md index a25673abf2..3838635c24 100644 --- a/docs/docs/administration/server-commands.md +++ b/docs/docs/administration/server-commands.md @@ -16,7 +16,7 @@ The `immich-server` docker image comes preinstalled with an administrative CLI ( ## How to run a command -To run a command, [connect](/docs/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich-admin `. +To run a command, [connect](/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich-admin `. ## Examples diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md index f241050136..fdfdad29ea 100644 --- a/docs/docs/administration/system-settings.md +++ b/docs/docs/administration/system-settings.md @@ -12,14 +12,14 @@ Manage password, OAuth, and other authentication settings ### OAuth Authentication -Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth). +Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/administration/oauth). ### Password Authentication -The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts. +The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts. :::tip -You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login. +You can always use the [Server CLI](/administration/server-commands) to re-enable password login. ::: ## Image Settings (thumbnails and previews) @@ -108,7 +108,7 @@ If more than one URL is provided, each server will be attempted one-at-a-time un ### Smart Search -The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change. +The [smart search](/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change. :::info Internet connection Changing models requires a connection to the Internet to download the model. @@ -132,7 +132,7 @@ Editable settings: - **Max Recognition Distance** - **Min Recognized Faces** -You can learn more about these options on the [Facial Recognition page](/docs/features/facial-recognition#how-face-detection-works) +You can learn more about these options on the [Facial Recognition page](/features/facial-recognition#how-face-detection-works) :::info When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces. @@ -154,15 +154,15 @@ The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for ### Reverse Geocoding Settings -Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database. +Immich supports [Reverse Geocoding](/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database. ## Notification Settings -SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/docs/administration/email-notification) +SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/administration/email-notification) ## Notification Templates -Override the default notifications text with notification templates. More information can be found [here](/docs/administration/email-notification) +Override the default notifications text with notification templates. More information can be found [here](/administration/email-notification) ## Server Settings @@ -176,7 +176,7 @@ The administrator can set a custom message on the login screen (the message will ## Storage Template -Immich supports a custom [Storage Template](/docs/administration/storage-template). Learn more about this feature and its configuration [here](/docs/administration/storage-template). +Immich supports a custom [Storage Template](/administration/storage-template). Learn more about this feature and its configuration [here](/administration/storage-template). ## Theme Settings diff --git a/docs/docs/developer/architecture.mdx b/docs/docs/developer/architecture.mdx index a8d38ba5c1..42d9c1b974 100644 --- a/docs/docs/developer/architecture.mdx +++ b/docs/docs/developer/architecture.mdx @@ -44,7 +44,7 @@ The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses ### CLI -The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/docs/features/command-line-interface.md) for more information. +The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/features/command-line-interface.md) for more information. ## Server @@ -83,11 +83,11 @@ Immich uses a [worker](https://github.com/immich-app/immich/blob/main/server/src - Smart Search - Facial Recognition - Storage Template Migration -- Sidecar (see [XMP Sidecars](/docs/features/xmp-sidecars.md)) +- Sidecar (see [XMP Sidecars](/features/xmp-sidecars.md)) - Background jobs (file deletion, user deletion) :::info -This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities. +This list closely matches what is available on the [Administration > Jobs](/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities. ::: ### Machine Learning diff --git a/docs/docs/developer/devcontainers.md b/docs/docs/developer/devcontainers.md index c7c48acf2b..0a1946e6c1 100644 --- a/docs/docs/developer/devcontainers.md +++ b/docs/docs/developer/devcontainers.md @@ -431,7 +431,7 @@ While the Dev Container focuses on server and web development, you can connect m - Server URL: `http://YOUR_IP:2283/api` - Ensure firewall allows port 2283 -3. **For full mobile development**, see the [mobile development guide](/docs/developer/setup) which covers: +3. **For full mobile development**, see the [mobile development guide](/developer/setup) which covers: - Flutter setup - Running on simulators/devices - Mobile-specific debugging @@ -474,7 +474,7 @@ Recommended minimums: ## Next Steps -- Read the [architecture overview](/docs/developer/architecture) -- Learn about [database migrations](/docs/developer/database-migrations) -- Explore [API documentation](/docs/api) +- Read the [architecture overview](/developer/architecture) +- Learn about [database migrations](/developer/database-migrations) +- Explore [API documentation](https://api.immich.app/) - Join `#immich` on [Discord](https://discord.immich.app) diff --git a/docs/docs/developer/open-api.md b/docs/docs/developer/open-api.md index 2c29c7365b..f627b2c459 100644 --- a/docs/docs/developer/open-api.md +++ b/docs/docs/developer/open-api.md @@ -1,6 +1,6 @@ # OpenAPI -Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](/docs/api). +Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/). ## Generator diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md index ea44367742..f855e854c4 100644 --- a/docs/docs/developer/pr-checklist.md +++ b/docs/docs/developer/pr-checklist.md @@ -53,8 +53,8 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss ## OpenAPI -The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/docs/developer/open-api.md) for more details. +The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/developer/open-api.md) for more details. ## Database Migrations -A database migration needs to be generated whenever there are changes to `server/src/infra/src/entities`. See [Database Migration](/docs/developer/database-migrations.md) for more details. +A database migration needs to be generated whenever there are changes to `server/src/infra/src/entities`. See [Database Migration](/developer/database-migrations.md) for more details. diff --git a/docs/docs/features/automatic-backup.md b/docs/docs/features/automatic-backup.md index 8fcbedaa6e..30d132cef8 100644 --- a/docs/docs/features/automatic-backup.md +++ b/docs/docs/features/automatic-backup.md @@ -16,7 +16,7 @@ If foreground backup is enabled: whenever the app is opened or resumed, it will ## Background backup -This feature is intended for everyday use. For initial bulk uploading, please use the foreground upload feature. For more information on why background upload is not working as expected, please refer to the [FAQ](/docs/FAQ#why-does-foreground-backup-stop-when-i-navigate-away-from-the-app-shouldnt-it-transfer-the-job-to-background-backup). +This feature is intended for everyday use. For initial bulk uploading, please use the foreground upload feature. For more information on why background upload is not working as expected, please refer to the [FAQ](/FAQ#why-does-foreground-backup-stop-when-i-navigate-away-from-the-app-shouldnt-it-transfer-the-job-to-background-backup). If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the server. If there are, it will upload them to the cloud in the background. diff --git a/docs/docs/features/facial-recognition.md b/docs/docs/features/facial-recognition.md index f0dec55484..85712ef5f6 100644 --- a/docs/docs/features/facial-recognition.md +++ b/docs/docs/features/facial-recognition.md @@ -70,7 +70,7 @@ Navigating to Administration > Settings > Machine Learning Settings > Facial Rec :::tip It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa. -You can learn how the tune the result in this [Guide](/docs/guides/better-facial-clusters) +You can learn how the tune the result in this [Guide](/guides/better-facial-clusters) ::: ### Facial recognition model diff --git a/docs/docs/features/img/xmp-sidecars.webp b/docs/docs/features/img/xmp-sidecars.webp deleted file mode 100644 index f00b32c730..0000000000 Binary files a/docs/docs/features/img/xmp-sidecars.webp and /dev/null differ diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index f274ca3c70..08f37c6821 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -33,7 +33,7 @@ Sometimes, an external library will not scan correctly. This can happen if Immic - Are the permissions set correctly? - Make sure you are using forward slashes (`/`) and not backward slashes. -To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the same in any microservices containers. +To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/mnt/photos`, check it with `ls /mnt/photos`. If you are using a dedicated microservices container, make sure to add the same mount point and check for availability within the microservices container as well. ### Exclusion Patterns @@ -103,7 +103,7 @@ The `immich-server` container will need access to the gallery. Modify your docke :::tip The `ro` flag at the end only gives read-only access to the volumes. -This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/docs/features/xmp-sidecars)). +This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/features/xmp-sidecars)). ::: :::info diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md index a94f8c8c64..086f93a000 100644 --- a/docs/docs/features/ml-hardware-acceleration.md +++ b/docs/docs/features/ml-hardware-acceleration.md @@ -35,7 +35,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele - Where and how you can get this file depends on device and vendor, but typically, the device vendor also supplies these - The `hwaccel.ml.yml` file assumes the path to it is `/usr/lib/libmali.so`, so update accordingly if it is elsewhere - The `hwaccel.ml.yml` file assumes an additional file `/lib/firmware/mali_csffw.bin`, so update accordingly if your device's driver does not require this file -- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for ARM NN specific settings +- Optional: Configure your `.env` file, see [environment variables](/install/environment-variables) for ARM NN specific settings - In particular, the `MACHINE_LEARNING_ANN_FP16_TURBO` can significantly improve performance at the cost of very slightly lower accuracy #### CUDA @@ -49,7 +49,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele - The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=
`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`. - The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached. -- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/docs/install/environment-variables) setting). +- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting). #### OpenVINO @@ -64,7 +64,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele - This is usually pre-installed on the device vendor's Linux images - RKNPU driver V0.9.8 or later must be available in the host server - You may confirm this by running `cat /sys/kernel/debug/rknpu/version` to check the version -- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for RKNN specific settings +- Optional: Configure your `.env` file, see [environment variables](/install/environment-variables) for RKNN specific settings - In particular, setting `MACHINE_LEARNING_RKNN_THREADS` to 2 or 3 can _dramatically_ improve performance for RK3576 and RK3588 compared to the default of 1, at the expense of multiplying the amount of RAM each model uses by that amount. ## Setup diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index cd837741f1..82a2976b41 100644 --- a/docs/docs/features/mobile-app.mdx +++ b/docs/docs/features/mobile-app.mdx @@ -28,7 +28,7 @@ The beta release channel allows users to test upcoming changes before they are o :::info -You can enable automatic backup on supported devices. For more information see [Automatic Backup](/docs/features/automatic-backup.md). +You can enable automatic backup on supported devices. For more information see [Automatic Backup](/features/automatic-backup.md). ::: ## Sync only selected photos @@ -75,7 +75,7 @@ You can sync or mirror an album from your phone to the Immich server on your acc - **User-Specific Sync:** Album synchronization is unique to each server user and does not sync between different users or partners. -- **Mobile-Only Feature:** Album synchronization is currently only available on mobile. For similar options on a computer, refer to [Libraries](/docs/features/libraries) for further details. +- **Mobile-Only Feature:** Album synchronization is currently only available on mobile. For similar options on a computer, refer to [Libraries](/features/libraries) for further details. ### Synchronizing albums from the past diff --git a/docs/docs/features/monitoring.md b/docs/docs/features/monitoring.md index 64377ec073..f087a3306f 100644 --- a/docs/docs/features/monitoring.md +++ b/docs/docs/features/monitoring.md @@ -28,7 +28,7 @@ The metrics in immich are grouped into API (endpoint calls and response times), Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_TELEMETRY_INCLUDE=all` environmental variable to your `.env` file. Note that only the server container currently use this variable. :::tip -`IMMICH_TELEMETRY_INCLUDE=all` enables all metrics. For a more granular configuration you can enumerate the telemetry metrics that should be included as a comma separated list (e.g. `IMMICH_TELEMETRY_INCLUDE=repo,api`). Alternatively, you can also exclude specific metrics with `IMMICH_TELEMETRY_EXCLUDE`. For more information refer to the [environment section](/docs/install/environment-variables.md#prometheus). +`IMMICH_TELEMETRY_INCLUDE=all` enables all metrics. For a more granular configuration you can enumerate the telemetry metrics that should be included as a comma separated list (e.g. `IMMICH_TELEMETRY_INCLUDE=repo,api`). Alternatively, you can also exclude specific metrics with `IMMICH_TELEMETRY_EXCLUDE`. For more information refer to the [environment section](/install/environment-variables.md#prometheus). ::: The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way. @@ -66,9 +66,9 @@ The provided file is just a starting point. There are a ton of ways to configure After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network. :::note -To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8082` to the microservices container's ports. +To see exactly what metrics are made available, you can additionally add `8081:8081` (API metrics) and `8082:8082` (microservices metrics) to the immich_server container's ports. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects. -To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general). +To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/install/environment-variables/#general). ::: ### Usage diff --git a/docs/docs/features/reverse-geocoding.md b/docs/docs/features/reverse-geocoding.md index 399bdd9b48..b1aee74a99 100644 --- a/docs/docs/features/reverse-geocoding.md +++ b/docs/docs/features/reverse-geocoding.md @@ -8,7 +8,7 @@ During Exif Extraction, assets with latitudes and longitudes are reverse geocode ## Usage -Data from a reverse geocode is displayed in the image details, and used in [Smart Search](/docs/features/searching.md). +Data from a reverse geocode is displayed in the image details, and used in [Smart Search](/features/searching.md). diff --git a/docs/docs/features/sharing.md b/docs/docs/features/sharing.md index ff0a03beea..9ba7470407 100644 --- a/docs/docs/features/sharing.md +++ b/docs/docs/features/sharing.md @@ -24,7 +24,7 @@ After creating an album, you can access the sharing options by clicking on the s Partner sharing allows you to share your _entire_ library with other users of your choice. They can then view your library and download the assets. -You can read this guide to learn more about [partner sharing](/docs/features/partner-sharing). +You can read this guide to learn more about [partner sharing](/features/partner-sharing). ## Public sharing diff --git a/docs/docs/features/tags.md b/docs/docs/features/tags.md index ca663e9edd..79a9696d9a 100644 --- a/docs/docs/features/tags.md +++ b/docs/docs/features/tags.md @@ -1,6 +1,6 @@ # Tags -Immich supports hierarchical tags, with the ability to read existing tags from the `TagList` and `Keywords` EXIF properties. Any changes to tags made through Immich are also written back to a [sidecar](/docs/features/xmp-sidecars) file. You can re-run the metadata extraction jobs for all assets to import your existing tags. +Immich supports hierarchical tags, with the ability to read existing tags from the XMP `TagsList` field and IPTC `Keywords` field. Any changes to tags made through Immich are also written back to a [sidecar](/features/xmp-sidecars) file. You can re-run the metadata extraction jobs for all assets to import your existing tags. ## Enable tags feature diff --git a/docs/docs/features/user-settings.md b/docs/docs/features/user-settings.md index a2d0308541..402105cd43 100644 --- a/docs/docs/features/user-settings.md +++ b/docs/docs/features/user-settings.md @@ -15,9 +15,9 @@ You can access the [user settings](https://my.immich.app/user-settings) by click --- :::tip Reset Password -The admin can reset a user password through the [User Management](/docs/administration/user-management.mdx) screen. +The admin can reset a user password through the [User Management](/administration/user-management.mdx) screen. ::: :::tip Reset Admin Password -The admin password can be reset using a [Server Command](/docs/administration/server-commands.md) +The admin password can be reset using a [Server Command](/administration/server-commands.md) ::: diff --git a/docs/docs/features/xmp-sidecars.md b/docs/docs/features/xmp-sidecars.md index 98ce8782e6..3536777d8a 100644 --- a/docs/docs/features/xmp-sidecars.md +++ b/docs/docs/features/xmp-sidecars.md @@ -1,13 +1,68 @@ # XMP Sidecars -Immich can ingest XMP sidecars on file upload (via the CLI) as well as detect new sidecars that are placed in the filesystem for existing images. +Immich supports XMP sidecar files — external `.xmp` files that store metadata for an image or video in XML format. During the metadata extraction job Immich will read & import metadata from `.xmp` files, and during the Sidecar Write job it will _write_ metadata back to `.xmp`. - +:::tip +Tools like Lightroom, Darktable, digiKam and other applications can also be configured to write changes to `.xmp` files, in order to avoid modifying the original file. +::: -XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the media file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary. +## Metadata Fields -When importing files via the CLI bulk uploader or parsing photo metadata for external libraries, Immich will automatically detect XMP sidecar files as files that exist next to the original media file. Immich will look files that have the same name as the photo, but with the `.xmp` file extension. The same name can either include the photo's file extension or without the photo's file extension. For example, for a photo named `PXL_20230401_203352928.MP.jpg`, Immich will look for an XMP file named either `PXL_20230401_203352928.MP.jpg.xmp` or `PXL_20230401_203352928.MP.xmp`. If both `PXL_20230401_203352928.MP.jpg.xmp` and `PXL_20230401_203352928.MP.xmp` are present, Immich will prefer `PXL_20230401_203352928.MP.jpg.xmp`. +Immich does not support _all_ metadata fields. Below is a table showing what fields Immich can _read_ and _write_. It's important to note that writes do not replace the entire file contents, but are merged together with any existing fields. -There are 2 administrator jobs associated with sidecar files: `SYNC` and `DISCOVER`. The sync job will re-scan all media with existing sidecar files and queue them for a metadata refresh. This is a great use case when third-party applications are used to modify the metadata of media. The discover job will attempt to scan the filesystem for new sidecar files for all media that does not currently have a sidecar file associated with it. +:::info +Immich automatically queues a Sidecar Write job after editing the description, rating, or updating tags. +::: - +| Metadata | Immich writes to XMP | Immich reads from XMP | +| --------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Description** | `dc:description`, `tiff:ImageDescription` | `dc:description`, `tiff:ImageDescription` | +| **Rating** | `xmp:Rating` | `xmp:Rating` | +| **DateTime** | `exif:DateTimeOriginal`, `photoshop:DateCreated` | In prioritized order:
`exif:SubSecDateTimeOriginal`
`exif:DateTimeOriginal`
`xmp:SubSecCreateDate`
`xmp:CreateDate`
`xmp:CreationDate`
`xmp:MediaCreateDate`
`xmp:SubSecMediaCreateDate`
`xmp:DateTimeCreated` | +| **Location** | `exif:GPSLatitude`, `exif:GPSLongitude` | `exif:GPSLatitude`, `exif:GPSLongitude` | +| **Tags** | `digiKam:TagsList` | In prioritized order:
`digiKam:TagsList`
`lr:HierarchicalSubject`
`IPTC:Keywords` | + +:::note +All other fields (e.g. `Creator`, `Source`, IPTC, Lightroom edits) remain in the `.xmp` file and are **not searchable** in Immich. +::: + +## File Naming Rules + +A sidecar must share the base name of the media file: + +- ✅ `IMG_0001.jpg.xmp` ← preferred +- ✅ `IMG_0001.xmp` ← fallback +- ❌ `myphoto_meta.xmp` ← not recognized + +If both `.jpg.xmp` and `.xmp` are present, Immich uses the **`.jpg.xmp`** file. + +## CLI Support + +1. **Detect** – Immich looks for a `.xmp` file placed next to each media file during upload. +2. **Copy** – Both the media and the sidecar file are copied into Immich’s internal library folder. + The sidecar is renamed to match the internal filename template, e.g.: + `upload/library//YYYY/YYYY-MM-DD/IMG_0001.jpg` + `upload/library//YYYY/YYYY-MM-DD/IMG_0001.jpg.xmp` +3. **Extract** – Selected metadata (title, description, date, rating, tags) is parsed from the sidecar and saved to the database. +4. **Write-back** – If you later update tags, rating, or description in the web UI, Immich will update **both** the database _and_ the copied `.xmp` file to stay in sync. + +## External Library (Mounted Folder) Support + +1. **Detect** – The `DISCOVER` job automatically associates `.xmp` files that sit next to existing media files in your mounted folder. No files are moved or renamed. +2. **Extract** – Immich reads and saves the same metadata fields from the sidecar to the database. +3. **Write-back** – If Immich has **write access** to the mount, any future metadata edits (e.g., rating or tags) are also written back to the original `.xmp` file on disk. + +:::danger +If the mount is **read-only**, Immich cannot update either the sidecar **or** the database — **metadata edits will silently fail** with no warning see issue [#10538](https://github.com/immich-app/immich/issues/10538) for more details. +::: + +## Admin Jobs + +Immich provides two admin jobs for managing sidecars: + +| Job | What it does | +| ---------- | ------------------------------------------------------------------------------------------------- | +| `DISCOVER` | Finds new `.xmp` files next to media that don’t already have one linked | +| `SYNC` | Re-reads existing `.xmp` files and refreshes metadata in the database (e.g. after external edits) | + +![Sidecar Admin Jobs](./img/sidecar-jobs.webp) diff --git a/docs/docs/guides/better-facial-clusters.md b/docs/docs/guides/better-facial-clusters.md index f4409b441c..40796983a5 100644 --- a/docs/docs/guides/better-facial-clusters.md +++ b/docs/docs/guides/better-facial-clusters.md @@ -10,7 +10,7 @@ This guide explains how to optimize facial recognition in systems with large ima - **Best Suited For:** Large image libraries after importing a significant number of images. - **Warning:** This method deletes all previously assigned names. -- **Tip:** **Always take a [backup](/docs/administration/backup-and-restore#database) before proceeding!** +- **Tip:** **Always take a [backup](/administration/backup-and-restore#database) before proceeding!** --- diff --git a/docs/docs/guides/custom-locations.md b/docs/docs/guides/custom-locations.md index af8ca438e7..e0274d3bd9 100644 --- a/docs/docs/guides/custom-locations.md +++ b/docs/docs/guides/custom-locations.md @@ -9,7 +9,7 @@ It is important to remember to update the backup settings after following the gu In our `.env` file, we will define the paths we want to use. Note that you don't have to define all of these: UPLOAD_LOCATION will be the base folder that files are stored in by default, with the other paths acting as overrides. ```diff title=".env" -# You can find documentation for all the supported environment variables [here](/docs/install/environment-variables) +# You can find documentation for all the supported environment variables [here](/install/environment-variables) # Custom location where your uploaded, thumbnails, and transcoded video files are stored - UPLOAD_LOCATION=./library diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 267e7bf2ad..5cdcdc04c4 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -7,7 +7,7 @@ Keep in mind that mucking around in the database might set the Moon on fire. Avo :::tip Run `docker exec -it immich_postgres psql --dbname= --username=` to connect to the database via the container directly. -(Replace `` and `` with the values from your [`.env` file](/docs/install/environment-variables#database)). +(Replace `` and `` with the values from your [`.env` file](/install/environment-variables#database)). ::: ## Assets @@ -142,12 +142,15 @@ DELETE FROM "person" WHERE "name" = 'PersonNameHere'; SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config'; ``` -(Only used when not using the [config file](/docs/install/config-file)) +(Only used when not using the [config file](/install/config-file)) ### File properties ```sql title="Without thumbnails" -SELECT * FROM "asset" WHERE "asset"."previewPath" IS NULL OR "asset"."thumbnailPath" IS NULL; +SELECT * FROM "asset" +WHERE (NOT EXISTS (SELECT 1 FROM "asset_file" WHERE "asset"."id" = "asset_file"."assetId" AND "asset_file"."type" = 'thumbnail') + OR NOT EXISTS (SELECT 1 FROM "asset_file" WHERE "asset"."id" = "asset_file"."assetId" AND "asset_file"."type" = 'preview')) +AND "asset"."visibility" = 'timeline'; ``` ```sql title="Failed file movements" diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 7921843297..ef467159e7 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -1,13 +1,13 @@ # External Library -This guide walks you through adding an [External Library](/docs/features/libraries). +This guide walks you through adding an [External Library](/features/libraries). This guide assumes you are running Immich in Docker and that the files you wish to access are stored in a directory on the same machine. # Mount the directory into the containers. Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`. -If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/docs/features/xmp-sidecars)), remove `:ro` from the end of the mount point. +If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/features/xmp-sidecars)), remove `:ro` from the end of the mount point. ```diff immich-server: diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index 6f401dfc5a..518b003c3a 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -46,7 +46,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side. -If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/docs/administration/reverse-proxy.md). +If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/administration/reverse-proxy.md). You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accessible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser. diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md index 72ae0e3fa1..0a8ddf2577 100644 --- a/docs/docs/guides/remote-machine-learning.md +++ b/docs/docs/guides/remote-machine-learning.md @@ -1,6 +1,6 @@ # Remote Machine Learning -To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user. +To alleviate [performance issues on low-memory systems](/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user. :::info Smart Search and Face Detection will use this feature, but Facial Recognition will not. This is because Facial Recognition uses the _outputs_ of these models that have already been saved to the database. As such, its processing is between the server container and the database. @@ -14,7 +14,7 @@ Image previews are sent to the remote machine learning container. Use this optio 2. Copy the following `docker-compose.yml` to the remote server :::info -If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/docs/features/ml-hardware-acceleration) +If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/features/ml-hardware-acceleration) ::: ```yaml diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md index 34381dd0ee..19647d4ae1 100644 --- a/docs/docs/guides/template-backup-script.md +++ b/docs/docs/guides/template-backup-script.md @@ -7,7 +7,7 @@ This script assumes you have a second hard drive connected to your server for on The database is saved to your Immich upload folder in the `database-backup` subdirectory. The database is then backed up and versioned with your assets by Borg. This ensures that the database backup is in sync with your assets in every snapshot. :::info -This script makes backups of your database along with your photo/video library. This is redundant with the [automatic database backup tool](https://immich.app/docs/administration/backup-and-restore#automatic-database-backups) built into Immich. Using this script to backup your database has two advantages over the built-in backup tool: +This script makes backups of your database along with your photo/video library. This is redundant with the [automatic database backup tool](/administration/backup-and-restore#automatic-database-dumps) built into Immich. Using this script to backup your database has two advantages over the built-in backup tool: - This script uses storage more efficiently by versioning your backups instead of making multiple copies. - The database backups are performed at the same time as the library backup, ensuring that the backups of your database and the library are always in sync. diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 54d7c61bb3..3fb0687e4a 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -209,7 +209,7 @@ So you can just grab it from there, paste it into a file and you're pretty much ### Step 2 - Specify the file location In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config. -For more information, refer to the [Environment Variables](/docs/install/environment-variables.md) section. +For more information, refer to the [Environment Variables](/install/environment-variables.md) section. :::tip YAML-formatted config files are also supported. diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 7a0b566f5d..46b144eb4a 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -29,4 +29,4 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc ## Next Steps -Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md). +Read the [Post Installation](/install/post-install.mdx) steps and [upgrade instructions](/install/upgrading.md). diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 928e0b26e5..e606d03dee 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -42,7 +42,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | -| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices | +| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. @@ -57,7 +57,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_WORKERS_EXCLUDE` | Do not run these workers. Matches against default workers, or `IMMICH_WORKERS_INCLUDE` if specified. | | server | :::info -Information on the current workers can be found [here](/docs/administration/jobs-workers). +Information on the current workers can be found [here](/administration/jobs-workers). ::: ## Ports @@ -169,8 +169,6 @@ Redis (Sentinel) URL example JSON before encoding: | `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | | `MACHINE_LEARNING_DEVICE_IDS`\*4 | Device IDs to use in multi-GPU environments | `0` | machine learning | | `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | -| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server | -| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server | | `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning | | `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning | diff --git a/docs/docs/install/one-click.md b/docs/docs/install/one-click.md new file mode 100644 index 0000000000..53fcb20d21 --- /dev/null +++ b/docs/docs/install/one-click.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 65 +--- + +# One-Click [Cloud Service] + +:::note +This version of Immich is provided via cloud service providers' one-click marketplaces. Hosting costs are set by the cloud service providers. +Support for these are provided by the individual cloud service providers. + +**Please report issues to the corresponding [Github Repository][github].** +::: + +## Installation + +Go to the provider's marketplace and choose Immich, then follow the provided instructions. + +## One-Click Immich marketplace providers + +### DigitalOcean + +https://marketplace.digitalocean.com/apps/immich + +### Vultr + +https://www.vultr.com/marketplace/apps/immich + +## Issues + +For issues, open an issue on the associated [GitHub Repository][github]. + +[github]: https://github.com/immich-app/immich/ diff --git a/docs/docs/install/portainer.md b/docs/docs/install/portainer.md index 916d89a0d5..07fd255292 100644 --- a/docs/docs/install/portainer.md +++ b/docs/docs/install/portainer.md @@ -45,5 +45,5 @@ alt="Dot Env Example" 11. Click on "**Deploy the stack**". :::tip -For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide. +For more information on how to use the application, please refer to the [Post Installation](/install/post-install.mdx) guide. ::: diff --git a/docs/docs/install/post-install.mdx b/docs/docs/install/post-install.mdx index 636274aaea..b30e91f3cd 100644 --- a/docs/docs/install/post-install.mdx +++ b/docs/docs/install/post-install.mdx @@ -44,6 +44,6 @@ A list of common steps to take after installing Immich include: ## Setting up optional features -- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich -- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding -- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich +- [External Libraries](/features/libraries.md): Adding your existing photo library to Immich +- [Hardware Transcoding](/features/hardware-transcoding.md): Speeding up video transcoding +- [Hardware-Accelerated Machine Learning](/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich diff --git a/docs/docs/install/script.md b/docs/docs/install/script.md index 93d1fb166c..ce05dc82d9 100644 --- a/docs/docs/install/script.md +++ b/docs/docs/install/script.md @@ -5,12 +5,12 @@ sidebar_position: 20 # Install script [Experimental] :::caution -This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.mdx). +This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/install/docker-compose.mdx). ::: ## Requirements -Follow the [requirements page](/docs/install/requirements) to get started. +Follow the [requirements page](/install/requirements) to get started. The install script only supports Linux operating systems and requires Docker to be already installed on the system. @@ -32,5 +32,5 @@ The web application and mobile app will be available at `http:// These are used to add custom configuration options or to enable specific features. -More information on available environment variables can be found in the **[environment variables documentation](/docs/install/environment-variables/)**. +More information on available environment variables can be found in the **[environment variables documentation](/install/environment-variables/)**. :::info Some environment variables are not available for the TrueNAS Community Edition app as they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings). @@ -242,7 +242,7 @@ alt="Add External Libraries with Additional Storage" className="border rounded-xl" /> -You may configure [external libraries](/docs/features/libraries) by mounting them using **Additional Storage**. +You may configure [external libraries](/features/libraries) by mounting them using **Additional Storage**. The dataset that contains your external library files must at least give **read** access to the user running Immich (Default: `apps` (UID 568), `apps` (GID 568)). If you want to be able to delete files or edit metadata in the external library using Immich, you will need to give the **modify** permission to the user running Immich. @@ -266,7 +266,7 @@ A general recommendation is to mount any external libraries to a path beginning This feature should only be used by advanced users. ::: -Immich can use multiple datasets for its storage, allowing you to manage your data more granularly, similar to the old storage configuration. This is useful if you want to separate your data into different datasets for performance or organizational reasons. There is a general guide for this [here](/docs/guides/custom-locations), but read on for the TrueNAS guide. +Immich can use multiple datasets for its storage, allowing you to manage your data more granularly, similar to the old storage configuration. This is useful if you want to separate your data into different datasets for performance or organizational reasons. There is a general guide for this [here](/guides/custom-locations), but read on for the TrueNAS guide. Each additional dataset has to give the permission **_modify_** to the user who will run Immich (Default: `apps` (UID 568), `apps` (GID 568)) As described in the [Setting up Storage Datasets](#setting-up-storage-datasets) section above, you have to create the datasets with the **Apps** preset to ensure the correct permissions are set, or you can set the permissions manually after creating the datasets. @@ -309,7 +309,7 @@ className="border rounded-xl" Both **CPU** and **Memory** are limits, not reservations. This means that Immich can use up to the specified amount of CPU threads and RAM, but it will not reserve that amount of resources at all times. The system will allocate resources as needed, and Immich will use less than the specified amount most of the time. -- Enable **GPU Configuration** options if you have a GPU or CPU with integrated graphics that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). +- Enable **GPU Configuration** options if you have a GPU or CPU with integrated graphics that you will use for [Hardware Transcoding](/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/features/ml-hardware-acceleration.md). The process for NVIDIA GPU passthrough requires additional steps. More details here: [GPU Passthrough Docs for TrueNAS Apps](https://apps.truenas.com/managing-apps/installing-apps/#gpu-passthrough) @@ -332,7 +332,7 @@ Click **Web Portal** on the **Application Info** widget, or go to the URL `http: After that, you can start using Immich to upload and manage your photos and videos. :::tip -For more information on how to use the application once installed, please refer to the [Post Install](/docs/install/post-install.mdx) guide. +For more information on how to use the application once installed, please refer to the [Post Install](/install/post-install.mdx) guide. ::: ## Edit App Settings @@ -347,7 +347,7 @@ For more information on how to use the application once installed, please refer ## Updating the App :::danger -Make sure to read the general [upgrade instructions](/docs/install/upgrading.md). +Make sure to read the general [upgrade instructions](/install/upgrading.md). ::: When updates become available, TrueNAS alerts and provides easy updates. diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index efb493f267..ca7263a1e8 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -125,13 +125,13 @@ alt="Go to Docker Tab and visit the address listed next to immich-web" :::tip -For more information on how to use the application once installed, please refer to the [Post Install](/docs/install/post-install.mdx) guide. +For more information on how to use the application once installed, please refer to the [Post Install](/install/post-install.mdx) guide. ::: ## Updating Steps :::danger -Make sure to read the general [upgrade instructions](/docs/install/upgrading.md). +Make sure to read the general [upgrade instructions](/install/upgrading.md). ::: Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman UI, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager. diff --git a/docs/docs/install/upgrading.md b/docs/docs/install/upgrading.md index d638a6f7d1..da95222911 100644 --- a/docs/docs/install/upgrading.md +++ b/docs/docs/install/upgrading.md @@ -4,9 +4,7 @@ sidebar_position: 95 # Upgrading -:::danger Read the release notes -Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. You should read the release notes prior to updating and take special care when using automated tools like [Watchtower][watchtower]. - +:::tip Breaking changes You can see versions that had breaking changes [here][breaking]. ::: @@ -40,7 +38,7 @@ If you do not deploy Immich using Docker Compose and see a deprecation warning f Immich has migrated off of the deprecated pgvecto.rs database extension to its successor, [VectorChord](https://github.com/tensorchord/VectorChord), which comes with performance improvements in almost every aspect. This section will guide you on how to make this change in a Docker Compose setup. -Before making any changes, please [back up your database](/docs/administration/backup-and-restore). While every effort has been made to make this migration as smooth as possible, there’s always a chance that something can go wrong. +Before making any changes, please [back up your database](/administration/backup-and-restore). While every effort has been made to make this migration as smooth as possible, there’s always a chance that something can go wrong. After making a backup, please modify your `docker-compose.yml` file with the following information. @@ -101,7 +99,7 @@ Please don’t hesitate to contact us on [GitHub](https://github.com/immich-app/ #### I have a separate PostgreSQL instance shared with multiple services. How can I switch to VectorChord? -Please see the [standalone PostgreSQL documentation](/docs/administration/postgres-standalone#migrating-to-vectorchord) for migration instructions. The migration path will be different depending on whether you’re currently using pgvecto.rs or pgvector, as well as whether Immich has superuser DB permissions. +Please see the [standalone PostgreSQL documentation](/administration/postgres-standalone#migrating-to-vectorchord) for migration instructions. The migration path will be different depending on whether you’re currently using pgvecto.rs or pgvector, as well as whether Immich has superuser DB permissions. #### Why are so many lines removed from the `docker-compose.yml` file? Does this mean the health check is removed? diff --git a/docs/docs/overview/help.md b/docs/docs/overview/help.md index f38ecde168..e6523547fa 100644 --- a/docs/docs/overview/help.md +++ b/docs/docs/overview/help.md @@ -6,7 +6,7 @@ sidebar_position: 6 Running into an issue or have a question? Try the following: -1. Check the [FAQs](/docs/FAQ.mdx). +1. Check the [FAQs](/FAQ.mdx). 2. Read through the [Release Notes][github-releases]. 3. Search through existing [GitHub Issues][github-issues]. 4. Open a help ticket on [Discord][discord-link]. diff --git a/docs/docs/overview/img/social-preview-light.webp b/docs/docs/overview/img/social-preview-light.webp deleted file mode 100644 index 3d088f6522..0000000000 Binary files a/docs/docs/overview/img/social-preview-light.webp and /dev/null differ diff --git a/docs/docs/overview/quick-start.mdx b/docs/docs/overview/quick-start.mdx index 28cee15007..d80a194ad2 100644 --- a/docs/docs/overview/quick-start.mdx +++ b/docs/docs/overview/quick-start.mdx @@ -13,7 +13,7 @@ to install and use it. - A system with at least 4GB of RAM and 2 CPU cores. - [Docker](https://docs.docker.com/engine/install/) -> For a more detailed list of requirements, see the [requirements page](/docs/install/requirements). +> For a more detailed list of requirements, see the [requirements page](/install/requirements). --- @@ -61,7 +61,7 @@ import MobileAppBackup from '/docs/partials/_mobile-app-backup.md'; The backup time differs depending on how many photos are on your mobile device. Large uploads may take quite a while. -To quickly get going, you can selectively upload few photos first, by following this [guide](/docs/features/mobile-app#sync-only-selected-photos). +To quickly get going, you can selectively upload few photos first, by following this [guide](/features/mobile-app#sync-only-selected-photos). You can select the **Jobs** tab to see Immich processing your photos. @@ -72,7 +72,7 @@ You can select the **Jobs** tab to see Immich processing your photos. ## Review the database backup and restore process Immich has built-in database backups. You can refer to the -[database backup](/docs/administration/backup-and-restore) for more information. +[database backup](/administration/backup-and-restore) for more information. :::danger The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`. @@ -86,8 +86,8 @@ You may decide you'd like to install the server a different way; the Install cat You may decide you'd like to add the _rest_ of your photos from Google Photos, even those not on your mobile device, via Google Takeout. You can use [immich-go](https://github.com/simulot/immich-go) for this. -You may want to [upload photos from your own archive](/docs/features/command-line-interface). +You may want to [upload photos from your own archive](/features/command-line-interface). -You may want to incorporate a pre-existing archive of photos from an [External Library](/docs/features/libraries); there's a [guide](/docs/guides/external-library) for that. +You may want to incorporate a pre-existing archive of photos from an [External Library](/features/libraries); there's a [guide](/guides/external-library) for that. -You may want your mobile device to [back photos up to your server automatically](/docs/features/automatic-backup). +You may want your mobile device to [back photos up to your server automatically](/features/automatic-backup). diff --git a/docs/docs/overview/support-the-project.md b/docs/docs/overview/support-the-project.md index a439893a7e..ae24a3f1ce 100644 --- a/docs/docs/overview/support-the-project.md +++ b/docs/docs/overview/support-the-project.md @@ -10,11 +10,11 @@ By far the easiest way to help make Immich better it to use it and report issues ## Translations -Support the project by localizing on [Weblate](https://hosted.weblate.org/projects/immich/immich/). For more information, see the [Translations](/docs/developer/translations) section. +Support the project by localizing on [Weblate](https://hosted.weblate.org/projects/immich/immich/). For more information, see the [Translations](/developer/translations) section. ## Development -If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.mdx) section. +If you are a programmer or developer, take a look at Immich's [technology stack](/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/developer/architecture.mdx) section. ## Purchase Immich diff --git a/docs/docs/overview/welcome.mdx b/docs/docs/overview/welcome.mdx deleted file mode 100644 index 93ce705369..0000000000 --- a/docs/docs/overview/welcome.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Welcome to Immich - -Immich - Self-hosted photos and videos backup tool - -## Welcome! - -Hello, I am glad you are here. - -My name is Alex. I am an Electrical Engineer by schooling, then turned into a Software Engineer by trade and the pure love of problem solving. - -We were lying in bed with our newborn, and my wife said, "We are starting to accumulate a lot of photos and videos of our baby, and I don't want to pay for **_App-Which-Must-Not-Be-Named_** anymore. You always want to build something for me, so why don't you build me an app which can do that?" - -That was how the idea started to grow in my head. After that, I began to find existing solutions in the self-hosting space with similar backup functionality and the performance level of the **_App-Which-Must-Not-Be-Named_**. I found that the current solutions mainly focus on the gallery-type application. However, I want a simple-to-use backup tool with a native mobile app that can view photos and videos efficiently. So I set sail on this journey as a hungry engineer on the hunt. - -Another motivation that pushed me to deliver my execution of the **_App-Which-Must-Not-Be-Named_** alternative or replacement is for contributing back to the open source community that I have greatly benefited from over the years. - -I'm proud to share this creation with you, which values privacy, memories, and the joy of looking back at those moments in an easy-to-use and friendly interface. - -If you like the application or it helps you in some way, please consider [supporting](./support-the-project.md) the project. It will help me to continue to develop and maintain the application. diff --git a/docs/docs/partials/_server-backup.md b/docs/docs/partials/_server-backup.md index b9479600aa..34e09670e9 100644 --- a/docs/docs/partials/_server-backup.md +++ b/docs/docs/partials/_server-backup.md @@ -1,7 +1,6 @@ Now that you have imported some pictures, you should setup server backups to preserve your memories. -You can do so by following our [backup guide](/docs/administration/backup-and-restore.md). +You can do so by following our [backup guide](/administration/backup-and-restore.md). -:::danger -Immich is still under heavy development _and_ handles very important data. -It is essential that you set up good backups, and test them. +:::info +A 3-2-1 backup strategy is still crucial. The team has the responsibility to ensure that the application doesn’t cause loss of your precious memories; however, we cannot guarantee that hard drives will not fail, or an electrical event causes unexpected shutdown of your server/system, leading to data loss. Therefore, we still encourage users to follow best practices when safeguarding their data. Keep multiple copies of your most precious data: at least two local copies and one copy offsite in cold storage. ::: diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md index 20e9caac43..84236e0ac1 100644 --- a/docs/docs/partials/_storage-template.md +++ b/docs/docs/partials/_storage-template.md @@ -1,7 +1,7 @@ -Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/docs/administration/user-management/#set-storage-label-for-user). +Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/administration/user-management/#set-storage-label-for-user). :::tip -You can read more about the differences between storage template engine on and off [here](/docs/administration/backup-and-restore#asset-types-and-storage-locations) +You can read more about the differences between storage template engine on and off [here](/administration/backup-and-restore#asset-types-and-storage-locations) ::: The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d612dda253..70e0189a00 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -7,7 +7,7 @@ const prism = require('prism-react-renderer'); const config = { title: 'Immich', tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone', - url: 'https://immich.app', + url: 'https://docs.immich.app', baseUrl: '/', onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', @@ -42,26 +42,19 @@ const config = { ], presets: [ [ - 'docusaurus-preset-openapi', - /** @type {import('docusaurus-preset-openapi').Options} */ + 'classic', + /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { showLastUpdateAuthor: true, showLastUpdateTime: true, + routeBasePath: '/', sidebarPath: require.resolve('./sidebars.js'), // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: 'https://github.com/immich-app/immich/tree/main/docs/', }, - api: { - path: '../open-api/immich-openapi-specs.json', - routeBasePath: '/docs/api', - }, - // blog: { - // showReadingTime: true, - // editUrl: "https://github.com/immich-app/immich/tree/main/docs/", - // }, theme: { customCss: require.resolve('./src/css/custom.css'), }, @@ -72,11 +65,6 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ - announcementBar: { - id: 'site_announcement_immich', - content: `⚠️ The project is under very active development. Expect bugs and changes. Do not use it as the only way to store your photos and videos!`, - isCloseable: false, - }, docs: { sidebar: { autoCollapseCategories: false, @@ -95,17 +83,17 @@ const config = { position: 'right', }, { - to: '/docs/overview/welcome', + to: '/overview/quick-start', position: 'right', label: 'Docs', }, { - to: '/roadmap', + href: 'https://immich.app/roadmap', position: 'right', label: 'Roadmap', }, { - to: '/docs/api', + href: 'https://api.immich.app/', position: 'right', label: 'API', }, @@ -139,16 +127,16 @@ const config = { title: 'Overview', items: [ { - label: 'Welcome', - to: '/docs/overview/welcome', + label: 'Quick start', + to: '/overview/quick-start', }, { label: 'Installation', - to: '/docs/install/requirements', + to: '/install/requirements', }, { label: 'Contributing', - to: '/docs/overview/support-the-project', + to: '/overview/support-the-project', }, { label: 'Privacy Policy', @@ -161,15 +149,15 @@ const config = { items: [ { label: 'Roadmap', - to: '/roadmap', + href: 'https://immich.app/roadmap', }, { label: 'API', - to: '/docs/api', + href: 'https://api.immich.app/', }, { label: 'Cursed Knowledge', - to: '/cursed-knowledge', + href: 'https://immich.app/cursed-knowledge', }, ], }, diff --git a/docs/package.json b/docs/package.json index 7fbcbada5a..d984427622 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,10 +24,7 @@ "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", "autoprefixer": "^10.4.17", - "classnames": "^2.3.2", - "clsx": "^2.0.0", "docusaurus-lunr-search": "^3.3.2", - "docusaurus-preset-openapi": "^0.7.5", "lunr": "^2.3.9", "postcss": "^8.4.25", "prism-react-renderer": "^2.3.1", @@ -60,6 +57,6 @@ "node": ">=20" }, "volta": { - "node": "22.18.0" + "node": "22.20.0" } } diff --git a/docs/src/components/community-guides.tsx b/docs/src/components/community-guides.tsx index 49ba7a8a08..08c8e096d9 100644 --- a/docs/src/components/community-guides.tsx +++ b/docs/src/components/community-guides.tsx @@ -28,6 +28,12 @@ const guides: CommunityGuidesProps[] = [ description: `synchronize folders in imported library with albums having the folders name.`, url: 'https://github.com/immich-app/immich/discussions/3382', }, + { + title: 'Immich Podman Quadlets Handbook', + description: + 'A rewrite of the original Immich Docker Compose file using Podman Quadlets, with a set of extra guides in the repository’s wiki.', + url: 'https://github.com/linux-universe/immich-podman-quadlets/blob/main/README.md', + }, { title: 'Podman/Quadlets Install', description: 'Documentation for simple podman setup using quadlets.', diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 46e28b3b76..efce831df0 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -105,6 +105,21 @@ const projects: CommunityProjectProps[] = [ description: 'Speed up your machine learning by load balancing your requests to multiple computers', url: 'https://github.com/apetersson/immich_ml_balancer', }, + { + title: 'Immich Drop Uploader', + description: 'A tiny, zero-login web app for collecting photos/videos from anyone into your Immich server.', + url: 'https://github.com/Nasogaa/immich-drop', + }, + { + title: 'Immich Birthday Sync', + description: 'Bulk-upload and -download birthdays, with CardDAV sync support', + url: 'https://github.com/sid3windr/immich-birthday', + }, + { + title: 'Immich Stack', + description: 'Auto-stack photos with identical filenames and differing extensions (i.e. JPG+RAW)', + url: 'https://github.com/sid3windr/immich-stack', + }, ]; function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element { diff --git a/docs/src/components/version-switcher.tsx b/docs/src/components/version-switcher.tsx index 5cb23891aa..739d7bd001 100644 --- a/docs/src/components/version-switcher.tsx +++ b/docs/src/components/version-switcher.tsx @@ -11,7 +11,7 @@ export default function VersionSwitcher(): JSX.Element { useEffect(() => { async function getVersions() { try { - let baseUrl = 'https://immich.app'; + let baseUrl = 'https://docs.immich.app'; if (window.location.origin === 'http://localhost:3005') { baseUrl = window.location.origin; } @@ -21,12 +21,13 @@ export default function VersionSwitcher(): JSX.Element { const archiveVersions = await response.json(); const allVersions = [ - { label: 'Next', url: 'https://main.preview.immich.app' }, - { label: 'Latest', url: 'https://immich.app' }, + { label: 'Next', url: 'https://docs.main.preview.immich.app' }, + { label: 'Latest', url: 'https://docs.immich.app' }, ...archiveVersions, - ].map(({ label, url }) => ({ + ].map(({ label, url, rootPath }) => ({ label, url: new URL(url), + rootPath, })); setVersions(allVersions); @@ -50,12 +51,18 @@ export default function VersionSwitcher(): JSX.Element { className="version-switcher-34ab39" label={activeLabel} mobile={windowSize === 'mobile'} - items={versions.map(({ label, url }) => ({ - label, - to: new URL(location.pathname + location.search + location.hash, url).href, - target: '_self', - className: label === activeLabel ? 'dropdown__link--active menu__link--active' : '', // workaround because React Router `` only supports using URL path for checking if active: https://v5.reactrouter.com/web/api/NavLink/isactive-func - }))} + items={versions.map(({ label, url, rootPath }) => { + let path = location.pathname + location.search + location.hash; + if (rootPath && !path.startsWith(rootPath)) { + path = rootPath + path; + } + return { + label, + to: new URL(path, url).href, + target: '_self', + className: label === activeLabel ? 'dropdown__link--active menu__link--active' : '', // workaround because React Router `` only supports using URL path for checking if active: https://v5.reactrouter.com/web/api/NavLink/isactive-func + }; + })} /> ) ); diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx deleted file mode 100644 index f3dacc2ce6..0000000000 --- a/docs/src/pages/cursed-knowledge.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { - mdiBug, - mdiCalendarToday, - mdiCrosshairsOff, - mdiCrop, - mdiDatabase, - mdiLeadPencil, - mdiLockOff, - mdiLockOutline, - mdiMicrosoftWindows, - mdiSecurity, - mdiSpeedometerSlow, - mdiTrashCan, - mdiWeb, - mdiWrap, - mdiCloudKeyOutline, - mdiRegex, - mdiCodeJson, - mdiClockOutline, - mdiAccountOutline, - mdiRestart, -} from '@mdi/js'; -import Layout from '@theme/Layout'; -import React from 'react'; -import { Timeline, Item as TimelineItem } from '../components/timeline'; - -const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language); - -type Item = Omit & { date: Date }; - -const items: Item[] = [ - { - icon: mdiClockOutline, - iconColor: 'gray', - title: 'setTimeout is cursed', - description: - 'The setTimeout method in JavaScript is cursed when used with small values because the implementation may or may not actually wait the specified time.', - link: { - url: 'https://github.com/immich-app/immich/pull/20655', - text: '#20655', - }, - date: new Date(2025, 7, 4), - }, - { - icon: mdiAccountOutline, - iconColor: '#DAB1DA', - title: 'PostgreSQL USER is cursed', - description: - 'The USER keyword in PostgreSQL is cursed because you can select from it like a table, which leads to confusion if you have a table name user as well.', - link: { - url: 'https://github.com/immich-app/immich/pull/19891', - text: '#19891', - }, - date: new Date(2025, 7, 4), - }, - { - icon: mdiRestart, - iconColor: '#8395e3', - title: 'PostgreSQL RESET is cursed', - description: - 'PostgreSQL RESET is cursed because it is impossible to RESET a PostgreSQL extension parameter if the extension has been uninstalled.', - link: { - url: 'https://github.com/immich-app/immich/pull/19363', - text: '#19363', - }, - date: new Date(2025, 5, 20), - }, - { - icon: mdiRegex, - iconColor: 'purple', - title: 'Zitadel Actions are cursed', - description: - "Zitadel is cursed because its custom scripting feature is executed with a JS engine that doesn't support regex named capture groups.", - link: { - url: 'https://github.com/dop251/goja', - text: 'Go JS engine', - }, - date: new Date(2025, 5, 4), - }, - { - icon: mdiCloudKeyOutline, - iconColor: '#0078d4', - title: 'Entra is cursed', - description: - "Microsoft Entra supports PKCE, but doesn't include it in its OpenID discovery document. This leads to clients thinking PKCE isn't available.", - link: { - url: 'https://github.com/immich-app/immich/pull/18725', - text: '#18725', - }, - date: new Date(2025, 4, 30), - }, - { - icon: mdiCrop, - iconColor: 'tomato', - title: 'Image dimensions in EXIF metadata are cursed', - description: - 'The dimensions in EXIF metadata can be different from the actual dimensions of the image, causing issues with cropping and resizing.', - link: { - url: 'https://github.com/immich-app/immich/pull/17974', - text: '#17974', - }, - date: new Date(2025, 4, 5), - }, - { - icon: mdiCodeJson, - iconColor: 'yellow', - title: 'YAML whitespace is cursed', - description: 'YAML whitespaces are often handled in unintuitive ways.', - link: { - url: 'https://github.com/immich-app/immich/pull/17309', - text: '#17309', - }, - date: new Date(2025, 3, 1), - }, - { - icon: mdiMicrosoftWindows, - iconColor: '#357EC7', - title: 'Hidden files in Windows are cursed', - description: - 'Hidden files in Windows cannot be opened with the "w" flag. That, combined with SMB option "hide dot files" leads to a lot of confusion.', - link: { - url: 'https://github.com/immich-app/immich/pull/12812', - text: '#12812', - }, - date: new Date(2024, 8, 20), - }, - { - icon: mdiWrap, - iconColor: 'gray', - title: 'Carriage returns in bash scripts are cursed', - description: 'Git can be configured to automatically convert LF to CRLF on checkout and CRLF breaks bash scripts.', - link: { - url: 'https://github.com/immich-app/immich/pull/11613', - text: '#11613', - }, - date: new Date(2024, 7, 7), - }, - { - icon: mdiLockOff, - iconColor: 'red', - title: 'Fetch inside Cloudflare Workers is cursed', - description: - 'Fetch requests in Cloudflare Workers use http by default, even if you explicitly specify https, which can often cause redirect loops.', - link: { - url: 'https://community.cloudflare.com/t/does-cloudflare-worker-allow-secure-https-connection-to-fetch-even-on-flexible-ssl/68051/5', - text: 'Cloudflare', - }, - date: new Date(2024, 7, 7), - }, - { - icon: mdiCrosshairsOff, - iconColor: 'gray', - title: 'GPS sharing on mobile is cursed', - description: - 'Some phones will silently strip GPS data from images when apps without location permission try to access them.', - link: { - url: 'https://github.com/immich-app/immich/discussions/11268', - text: '#11268', - }, - date: new Date(2024, 6, 21), - }, - { - icon: mdiLeadPencil, - iconColor: 'gold', - title: 'PostgreSQL NOTIFY is cursed', - description: - 'PostgreSQL does everything in a transaction, including NOTIFY. This means using the socket.io postgres-adapter writes to WAL every 5 seconds.', - link: { url: 'https://github.com/immich-app/immich/pull/10801', text: '#10801' }, - date: new Date(2024, 6, 3), - }, - { - icon: mdiWeb, - iconColor: 'lightskyblue', - title: 'npm scripts are cursed', - description: - 'npm scripts make a http call to the npm registry each time they run, which means they are a terrible way to execute a health check.', - link: { url: 'https://github.com/immich-app/immich/issues/10796', text: '#10796' }, - date: new Date(2024, 6, 3), - }, - { - icon: mdiSpeedometerSlow, - iconColor: 'brown', - title: '50 extra packages are cursed', - description: - 'There is a user in the JavaScript community who goes around adding "backwards compatibility" to projects. They do this by adding 50 extra package dependencies to your project, which are maintained by them.', - link: { url: 'https://github.com/immich-app/immich/pull/10690', text: '#10690' }, - date: new Date(2024, 5, 28), - }, - { - icon: mdiLockOutline, - iconColor: 'gold', - title: 'Long passwords are cursed', - description: - 'The bcrypt implementation only uses the first 72 bytes of a string. Any characters after that are ignored.', - // link: GHSA-4p64-9f7h-3432 - date: new Date(2024, 5, 25), - }, - { - icon: mdiCalendarToday, - iconColor: 'greenyellow', - title: 'JavaScript Date objects are cursed', - description: 'JavaScript date objects are 1 indexed for years and days, but 0 indexed for months.', - link: { url: 'https://github.com/immich-app/immich/pull/6787', text: '#6787' }, - date: new Date(2024, 0, 31), - }, - { - icon: mdiBug, - iconColor: 'green', - title: 'ESM imports are cursed', - description: - 'Prior to Node.js v20.8 using --experimental-vm-modules in a CommonJS project that imported an ES module that imported a CommonJS modules would create a segfault and crash Node.js', - link: { - url: 'https://github.com/immich-app/immich/pull/6719', - text: '#6179', - }, - date: new Date(2024, 0, 9), - }, - { - icon: mdiDatabase, - iconColor: 'gray', - title: 'PostgreSQL parameters are cursed', - description: `PostgresSQL has a limit of ${Number(65535).toLocaleString()} parameters, so bulk inserts can fail with large datasets.`, - link: { - url: 'https://github.com/immich-app/immich/pull/6034', - text: '#6034', - }, - date: new Date(2023, 11, 28), - }, - { - icon: mdiSecurity, - iconColor: 'gold', - title: 'Secure contexts are cursed', - description: `Some web features like the clipboard API only work in "secure contexts" (ie. https or localhost)`, - link: { - url: 'https://github.com/immich-app/immich/issues/2981', - text: '#2981', - }, - date: new Date(2023, 5, 26), - }, - { - icon: mdiTrashCan, - iconColor: 'gray', - title: 'TypeORM deletes are cursed', - description: `The remove implementation in TypeORM mutates the input, deleting the id property from the original object.`, - link: { - url: 'https://github.com/typeorm/typeorm/issues/7024#issuecomment-948519328', - text: 'typeorm#6034', - }, - date: new Date(2023, 1, 23), - }, -]; - -export default function CursedKnowledgePage(): JSX.Element { - return ( - -
-

- Cursed Knowledge -

-

- Cursed knowledge we have learned as a result of building Immich that we wish we never knew. -

-
- b.date.getTime() - a.date.getTime()) - .map((item) => ({ ...item, getDateLabel: withLanguage(item.date) }))} - /> -
-
-
- ); -} diff --git a/docs/src/pages/errors.md b/docs/src/pages/errors.md index 5f73162a61..fed72f21c7 100644 --- a/docs/src/pages/errors.md +++ b/docs/src/pages/errors.md @@ -2,7 +2,17 @@ ## TypeORM Upgrade -In order to update to Immich to `v1.137.0` (or above), the application must be started at least once on a version in the range between `1.132.0` and `1.136.0`. Doing so will complete database schema upgrades that are required for `v1.137.0` (and above). After Immich has successfully updated to a version in this range, you can now attempt to update to v1.137.0 (or above). We recommend users upgrade to `1.132.0` since it does not have any other breaking changes. +If you encountered "Migrations failed: Error: Invalid upgrade path" then perform an intermediate upgrade to `v1.132.3` first. + +:::tip +We recommend users upgrade to `v1.132.3` since it does not have any breaking changes or bugs on this upgrade path. +::: + +In order to update to Immich `v1.137.0` or above, the application must be started at least once on a version in the range between `1.132.0` and `1.136.0`. Doing so will complete database schema upgrades that are required for `v1.137.0` (and above). After Immich has successfully updated to a version in this range, you can now attempt to update to `v1.137.0` (or above). + +:::caution +Avoid `v1.136.0` if upgrading from `v1.131.0` (or earlier) due to a bug blocking this upgrade in some installations. +::: ## Inconsistent Media Location diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 277a1d0b46..37455cde16 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -1,123 +1,5 @@ -import React from 'react'; -import Link from '@docusaurus/Link'; -import Layout from '@theme/Layout'; -import { discordPath, discordViewBox } from '@site/src/components/svg-paths'; -import ThemedImage from '@theme/ThemedImage'; -import Icon from '@mdi/react'; - -function HomepageHeader() { - return ( -
-
- Immich logo -
-
-
- - - - -
-

- Self-hosted{' '} - - photo and - video management{' '} - - solution -

- -

- Easily back up, organize, and manage your photos on your own server. Immich helps you - browse, search and organize your photos and videos with ease, without - sacrificing your privacy. -

-
-
- - Get Started - - - - Open Demo - -
- -
- - Join our Discord -
- -
-
-
- -
-

Download the mobile app

-

- Download the Immich app and start backing up your photos and videos securely to your own server -

-
-
-
- - Get it on Google Play - -
- -
- - Download on the App Store - -
- -
- - Download APK - -
-
- -
-
- ); -} +import { Redirect } from '@docusaurus/router'; export default function Home(): JSX.Element { - return ( - - -
-

This project is available under GNU AGPL v3 license.

-

Privacy should not be a luxury

-
-
- ); + return ; } diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx deleted file mode 100644 index e002c4d032..0000000000 --- a/docs/src/pages/roadmap.tsx +++ /dev/null @@ -1,944 +0,0 @@ -import { - mdiAccountGroup, - mdiAccountGroupOutline, - mdiAndroid, - mdiAppleIos, - mdiArchiveOutline, - mdiBash, - mdiBookSearchOutline, - mdiBookmark, - mdiCakeVariant, - mdiCameraBurst, - mdiChartBoxMultipleOutline, - mdiCheckAll, - mdiCheckboxMarked, - mdiCloudUploadOutline, - mdiCollage, - mdiContentDuplicate, - mdiCrop, - mdiDevices, - mdiEmailOutline, - mdiExpansionCard, - mdiEyeOutline, - mdiEyeRefreshOutline, - mdiFaceMan, - mdiFaceManOutline, - mdiFile, - mdiFileSearch, - mdiFlash, - mdiFolder, - mdiFolderMultiple, - mdiForum, - mdiHandshakeOutline, - mdiHeart, - mdiHistory, - mdiImage, - mdiImageAlbum, - mdiImageEdit, - mdiImageMultipleOutline, - mdiImageSearch, - mdiKeyboardSettingsOutline, - mdiLicense, - mdiLockOutline, - mdiMagnify, - mdiMagnifyScan, - mdiMap, - mdiMaterialDesign, - mdiMatrix, - mdiMerge, - mdiMonitor, - mdiMotionPlayOutline, - mdiPalette, - mdiPanVertical, - mdiPartyPopper, - mdiPencil, - mdiRaw, - mdiRocketLaunch, - mdiRotate360, - mdiScaleBalance, - mdiSecurity, - mdiServer, - mdiShare, - mdiShareAll, - mdiShareCircle, - mdiStar, - mdiStarOutline, - mdiTableKey, - mdiTag, - mdiTagMultiple, - mdiText, - mdiThemeLightDark, - mdiTrashCanOutline, - mdiVectorCombine, - mdiFolderSync, - mdiFaceRecognition, - mdiVideo, - mdiWeb, - mdiDatabaseOutline, - 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.135.0': new Date(2025, 5, 18), - '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), - 'v1.120.0': new Date(2024, 10, 6), - 'v1.114.0': new Date(2024, 8, 6), - 'v1.113.0': new Date(2024, 7, 30), - 'v1.112.0': new Date(2024, 7, 14), - 'v1.111.0': new Date(2024, 6, 26), - 'v1.110.0': new Date(2024, 5, 11), - 'v1.109.0': new Date(2024, 6, 18), - 'v1.106.1': new Date(2024, 5, 11), - 'v1.104.0': new Date(2024, 4, 13), - 'v1.103.0': new Date(2024, 3, 29), - 'v1.102.0': new Date(2024, 3, 15), - 'v1.99.0': new Date(2024, 2, 20), - 'v1.98.0': new Date(2024, 2, 7), - 'v1.95.0': new Date(2024, 1, 20), - 'v1.94.0': new Date(2024, 0, 31), - 'v1.93.0': new Date(2024, 0, 19), - 'v1.91.0': new Date(2023, 11, 15), - 'v1.90.0': new Date(2023, 11, 7), - 'v1.88.0': new Date(2023, 10, 20), - 'v1.84.0': new Date(2023, 10, 1), - 'v1.83.0': new Date(2023, 9, 28), - 'v1.82.0': new Date(2023, 9, 17), - 'v1.79.0': new Date(2023, 8, 21), - 'v1.76.0': new Date(2023, 7, 29), - 'v1.75.0': new Date(2023, 7, 26), - 'v1.72.0': new Date(2023, 7, 6), - 'v1.71.0': new Date(2023, 6, 29), - 'v1.69.0': new Date(2023, 6, 23), - 'v1.68.0': new Date(2023, 6, 20), - 'v1.67.0': new Date(2023, 6, 14), - 'v1.66.0': new Date(2023, 6, 4), - 'v1.65.0': new Date(2023, 5, 30), - 'v1.63.0': new Date(2023, 5, 24), - 'v1.61.0': new Date(2023, 5, 16), - 'v1.58.0': new Date(2023, 4, 28), - 'v1.57.0': new Date(2023, 4, 23), - 'v1.56.0': new Date(2023, 4, 18), - 'v1.55.0': new Date(2023, 4, 9), - 'v1.54.0': new Date(2023, 3, 18), - 'v1.52.0': new Date(2023, 2, 29), - 'v1.51.0': new Date(2023, 2, 20), - 'v1.48.0': new Date(2023, 1, 21), - 'v1.47.0': new Date(2023, 1, 13), - 'v1.46.0': new Date(2023, 1, 9), - 'v1.43.0': new Date(2023, 1, 3), - 'v1.41.0': new Date(2023, 0, 10), - 'v1.39.0': new Date(2022, 11, 19), - 'v1.36.0': new Date(2022, 10, 20), - 'v1.33.1': new Date(2022, 9, 26), - 'v1.32.0': new Date(2022, 9, 14), - 'v1.27.0': new Date(2022, 8, 6), - 'v1.24.0': new Date(2022, 7, 19), - 'v1.10.0': new Date(2022, 4, 29), - 'v1.7.0': new Date(2022, 3, 24), - 'v1.3.0': new Date(2022, 2, 22), - 'v1.2.0': new Date(2022, 1, 8), -} as const; - -const weirdTags = { - 'v1.41.0': 'v1.41.1_64-dev', - 'v1.39.0': 'v1.39.0_61-dev', - 'v1.36.0': 'v1.36.0_55-dev', - 'v1.33.1': 'v1.33.0_52-dev', - 'v1.32.0': 'v1.32.0_50-dev', - 'v1.27.0': 'v1.27.0_37-dev', - 'v1.24.0': 'v1.24.0_34-dev', - 'v1.10.0': 'v1.10.0_15-dev', - 'v1.7.0': 'v1.7.0_11-dev ', - 'v1.3.0': 'v1.3.0-dev ', - 'v1.2.0': 'v0.2-dev ', -}; - -const title = 'Roadmap'; -const description = 'A list of future plans and goals, as well as past achievements and milestones.'; - -const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language); - -type Base = { icon: string; iconColor?: React.CSSProperties['color']; title: string; description: string }; -const withRelease = ({ - icon, - iconColor, - title, - description, - release: version, -}: Base & { release: keyof typeof releases }) => { - return { - icon, - iconColor: iconColor ?? 'gray', - title, - description, - link: { - url: `https://github.com/immich-app/immich/releases/tag/${weirdTags[version] ?? version}`, - text: version, - }, - getDateLabel: withLanguage(releases[version]), - }; -}; - -const roadmap: Item[] = [ - { - done: false, - icon: mdiFlash, - iconColor: 'gold', - title: 'Workflows', - description: 'Automate tasks with workflows', - getDateLabel: () => 'Planned for 2025', - }, - { - done: false, - icon: mdiImageEdit, - iconColor: 'rebeccapurple', - title: 'Basic editor', - description: 'Basic photo editing capabilities', - getDateLabel: () => 'Planned for 2025', - }, - { - done: false, - icon: mdiRocketLaunch, - iconColor: 'indianred', - title: 'Stable release', - description: 'Immich goes stable', - getDateLabel: () => 'Planned for 2025', - }, - { - done: false, - icon: mdiCloudUploadOutline, - iconColor: 'cornflowerblue', - title: 'Better background backups', - description: 'Rework background backups to be more reliable', - getDateLabel: () => 'Planned for 2025', - }, - { - done: false, - icon: mdiCameraBurst, - iconColor: 'rebeccapurple', - title: 'Auto stacking', - description: 'Auto stack burst photos', - getDateLabel: () => 'Planned for 2025', - }, -]; - -const milestones: Item[] = [ - { - icon: mdiStar, - iconColor: 'gold', - title: '70,000 Stars', - description: 'Reached 70K Stars on GitHub!', - getDateLabel: withLanguage(new Date(2025, 6, 9)), - }, - withRelease({ - icon: mdiTableKey, - iconColor: 'gray', - title: 'Fine grained access controls', - description: 'Granular access controls for api keys', - release: 'v1.135.0', - }), - withRelease({ - icon: mdiCast, - iconColor: 'aqua', - title: 'Google Cast (web and mobile)', - description: 'Cast assets to Google Cast/Chromecast compatible devices', - release: 'v1.135.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', - title: 'Folders view in the mobile app', - description: 'Browse your photos and videos in their folder structure inside the mobile app', - release: 'v1.130.0', - }), - { - icon: mdiStar, - iconColor: 'gold', - title: '60,000 Stars', - description: 'Reached 60K Stars on GitHub!', - getDateLabel: withLanguage(new Date(2025, 2, 4)), - }, - withRelease({ - icon: mdiTagFaces, - iconColor: 'teal', - title: 'Manual face tagging', - description: - 'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.', - release: 'v1.127.0', - }), - withRelease({ - icon: mdiLinkEdit, - iconColor: 'crimson', - title: 'Automatic URL switching', - description: 'The mobile app now supports automatic switching between different server URLs', - release: 'v1.122.0', - }), - withRelease({ - icon: mdiMovieOpenPlayOutline, - iconColor: 'darksalmon', - title: 'Native video player', - description: 'HDR videos are now fully supported using the Immich native video player', - release: 'v1.122.0', - }), - withRelease({ - icon: mdiDatabaseOutline, - iconColor: 'brown', - title: 'Automatic database dumps', - description: 'Database dumps are now integrated into the Immich server', - release: 'v1.120.0', - }), - { - icon: mdiStar, - iconColor: 'gold', - title: '50,000 Stars', - description: 'Reached 50K Stars on GitHub!', - getDateLabel: withLanguage(new Date(2024, 10, 1)), - }, - withRelease({ - icon: mdiFaceRecognition, - title: 'Metadata Face Import', - description: 'Read face metadata in Digikam format during import', - release: 'v1.114.0', - }), - withRelease({ - icon: mdiTagMultiple, - iconColor: 'orange', - title: 'Tags', - description: 'Tag your photos and videos', - release: 'v1.113.0', - }), - withRelease({ - icon: mdiFolderSync, - iconColor: 'green', - title: 'Album sync (mobile)', - description: 'Sync or mirror an album from your phone to the Immich server', - release: 'v1.113.0', - }), - withRelease({ - icon: mdiFolderMultiple, - iconColor: 'brown', - title: 'Folders view', - description: 'Browse your photos and videos in their folder structure', - release: 'v1.113.0', - }), - withRelease({ - icon: mdiPalette, - title: 'Theming (mobile)', - description: 'Pick a primary color for the mobile app', - release: 'v1.112.0', - }), - withRelease({ - icon: mdiStarOutline, - iconColor: 'gold', - title: 'Star rating', - description: 'Rate your photos and videos', - release: 'v1.112.0', - }), - withRelease({ - icon: mdiCrop, - iconColor: 'royalblue', - title: 'Editor (mobile)', - description: 'Crop and rotate on mobile', - release: 'v1.111.0', - }), - withRelease({ - icon: mdiMap, - iconColor: 'green', - title: 'Deploy tiles.immich.cloud', - description: 'Dedicated tile server for Immich', - release: 'v1.111.0', - }), - { - icon: mdiStar, - iconColor: 'gold', - title: '40,000 Stars', - description: 'Reached 40K Stars on GitHub!', - getDateLabel: withLanguage(new Date(2024, 6, 21)), - }, - withRelease({ - icon: mdiShare, - title: 'Deploy my.immich.app', - description: 'Url router for immich links', - release: 'v1.109.0', - }), - withRelease({ - icon: mdiLicense, - iconColor: 'gold', - title: 'Supporter Badge', - description: 'The option to buy Immich to support its development!', - release: 'v1.109.0', - }), - withRelease({ - icon: mdiHistory, - title: 'Versioned documentation', - description: 'View documentation as it was at the time of past releases', - release: 'v1.106.1', - }), - withRelease({ - icon: mdiWeb, - iconColor: 'royalblue', - title: 'Web translations', - description: 'Translate the web application to multiple languages', - release: 'v1.106.1', - }), - withRelease({ - icon: mdiContentDuplicate, - title: 'Similar image detection', - description: "Detect duplicate assets that aren't exactly identical", - release: 'v1.106.1', - }), - withRelease({ - icon: mdiVectorCombine, - title: 'Container consolidation', - description: - 'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.', - release: 'v1.106.1', - }), - withRelease({ - icon: mdiPencil, - iconColor: 'saddlebrown', - title: 'Read-write external libraries', - description: 'Edit, update, and delete files in external libraries', - release: 'v1.104.0', - }), - withRelease({ - icon: mdiEmailOutline, - iconColor: 'crimson', - title: 'Email notifications', - description: 'Send emails for important events', - release: 'v1.104.0', - }), - { - icon: mdiHandshakeOutline, - iconColor: 'magenta', - title: 'Immich joins FUTO!', - description: 'Joined Futo and Immich core team goes full-time', - getDateLabel: withLanguage(new Date(2024, 4, 1)), - }, - withRelease({ - icon: mdiEyeOutline, - iconColor: 'darkslategray', - title: 'Read-only albums', - description: 'Share albums with other users as read-only', - release: 'v1.103.0', - }), - withRelease({ - icon: mdiBookmark, - iconColor: 'orangered', - title: 'Permanent URLs (Web)', - description: 'Assets on the web now have permanent URLs', - release: 'v1.103.0', - }), - withRelease({ - icon: mdiStar, - iconColor: 'gold', - title: '30,000 Stars', - description: 'Reached 30K Stars on GitHub!', - release: 'v1.102.0', - }), - withRelease({ - icon: mdiChartBoxMultipleOutline, - iconColor: 'mediumvioletred', - title: 'OpenTelemetry metrics', - description: 'OpenTelemetry metrics for local evaluation and advanced debugging', - release: 'v1.99.0', - }), - withRelease({ - icon: 'immich', - title: 'New logo', - description: 'Immich got its new logo', - release: 'v1.98.0', - }), - withRelease({ - icon: mdiMagnifyScan, - title: 'Search enhancement with advanced filters', - description: 'Advanced search with filters by date, location and more', - release: 'v1.95.0', - }), - withRelease({ - icon: mdiScaleBalance, - iconColor: 'gold', - title: 'AGPL License', - description: 'Immich switches to AGPLv3 license', - release: 'v1.95.0', - }), - withRelease({ - icon: mdiEyeRefreshOutline, - title: 'Library watching', - description: 'Automatically import files in external libraries when the operating system detects changes.', - release: 'v1.94.0', - }), - withRelease({ - icon: mdiExpansionCard, - iconColor: 'green', - title: 'GPU acceleration for machine-learning', - description: 'Hardware acceleration support for Nvidia and Intel devices through CUDA and OpenVINO.', - release: 'v1.94.0', - }), - withRelease({ - icon: mdiAccountGroupOutline, - iconColor: 'gray', - title: '250 unique contributors', - description: '250 amazing people contributed to Immich', - release: 'v1.93.0', - }), - withRelease({ - icon: mdiMatrix, - title: 'Search improvement with pgvecto.rs', - description: 'Moved the search from typesense to pgvecto.rs', - release: 'v1.91.0', - }), - withRelease({ - icon: mdiPencil, - iconColor: 'saddlebrown', - title: 'Edit metadata', - description: "Edit a photo or video's date, time, hours, timezone, and GPS information", - release: 'v1.90.0', - }), - withRelease({ - icon: mdiVectorCombine, - title: 'Container consolidation', - description: - 'The serving of the web app is merged into the server image, allowing us to remove two containers from the stack.', - release: 'v1.88.0', - }), - withRelease({ - icon: mdiBash, - iconColor: 'gray', - title: 'CLI v2', - description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.', - release: 'v1.88.0', - }), - withRelease({ - icon: mdiForum, - iconColor: 'dodgerblue', - title: 'Activity', - description: 'Comment a photo or a video in a shared album', - release: 'v1.84.0', - }), - withRelease({ - icon: mdiStar, - iconColor: 'gold', - title: '20,000 Stars', - description: 'Reached 20K Stars on GitHub!', - release: 'v1.83.0', - }), - withRelease({ - icon: mdiCameraBurst, - iconColor: 'rebeccapurple', - title: 'Stack assets', - description: 'Manual asset stacking for grouping and hiding related assets in the main timeline.', - release: 'v1.83.0', - }), - withRelease({ - icon: mdiPalette, - iconColor: 'magenta', - title: 'Custom theme', - description: 'Apply your custom CSS for modifying fonts, colors, and styles in the web application.', - release: 'v1.83.0', - }), - withRelease({ - icon: mdiTrashCanOutline, - iconColor: 'brown', - title: 'Trash feature', - description: 'Trash, restore from trash, and automatically empty the recycle bin after 30 days.', - release: 'v1.82.0', - }), - withRelease({ - icon: mdiBookSearchOutline, - title: 'External libraries', - description: 'Automatically import media into Immich based on imports paths and ignore patterns.', - release: 'v1.79.0', - }), - withRelease({ - icon: mdiMap, - iconColor: 'darksalmon', - title: 'Map view (mobile)', - description: 'Heat map implementation in the mobile app.', - release: 'v1.76.0', - }), - withRelease({ - icon: mdiFile, - iconColor: 'lightblue', - title: 'Configuration file', - description: 'Auto-configure an Immich installation via a configuration file.', - release: 'v1.75.0', - }), - withRelease({ - icon: mdiMonitor, - iconColor: 'darkcyan', - title: 'Slideshow mode (web)', - description: 'Start a full-screen slideshow from an Album on the web.', - release: 'v1.75.0', - }), - withRelease({ - icon: mdiServer, - iconColor: 'lightskyblue', - title: 'Hardware transcoding', - description: 'Support hardware acceleration (QuickSync, VAAPI, and Nvidia) for video transcoding.', - release: 'v1.72.0', - }), - withRelease({ - icon: mdiImageAlbum, - iconColor: 'olivedrab', - title: 'View albums via time buckets', - description: 'Upgrade albums to use time buckets, an optimized virtual viewport.', - release: 'v1.72.0', - }), - withRelease({ - icon: mdiImageAlbum, - iconColor: 'olivedrab', - title: 'Album description', - description: 'Save an album description.', - release: 'v1.72.0', - }), - withRelease({ - icon: mdiRotate360, - title: '360° Photos (web)', - description: 'View 360° Photos on the web.', - release: 'v1.71.0', - }), - withRelease({ - icon: mdiMotionPlayOutline, - title: 'Android motion photos', - description: 'Add support for Android Motion Photos.', - release: 'v1.69.0', - }), - withRelease({ - icon: mdiFaceManOutline, - iconColor: 'mistyrose', - title: 'Show/hide faces', - description: 'Add the options to show or hide faces.', - release: 'v1.68.0', - }), - withRelease({ - icon: mdiMerge, - iconColor: 'forestgreen', - title: 'Merge faces', - description: 'Add the ability to merge multiple faces together.', - release: 'v1.67.0', - }), - withRelease({ - icon: mdiImage, - iconColor: 'rebeccapurple', - title: 'Feature photo', - description: 'Add the option to change the feature photo for a person.', - release: 'v1.66.0', - }), - withRelease({ - icon: mdiKeyboardSettingsOutline, - iconColor: 'darkslategray', - title: 'Multi-select via SHIFT', - description: 'Add the option to multi-select while holding SHIFT.', - release: 'v1.66.0', - }), - withRelease({ - icon: mdiImageMultipleOutline, - iconColor: 'rebeccapurple', - title: 'Memories (mobile)', - description: 'View "On this day..." memories in the mobile app.', - release: 'v1.65.0', - }), - withRelease({ - icon: mdiFaceMan, - iconColor: 'mistyrose', - title: 'Facial recognition (mobile)', - description: 'View detected faces in the mobile app.', - release: 'v1.63.0', - }), - withRelease({ - icon: mdiImageMultipleOutline, - iconColor: 'rebeccapurple', - title: 'Memories (web)', - description: 'View pictures taken in past years on this day on the web.', - release: 'v1.61.0', - }), - withRelease({ - icon: mdiCollage, - iconColor: 'deeppink', - title: 'Justified layout (web)', - description: 'Implement justified layout (collage) on the web.', - release: 'v1.61.0', - }), - withRelease({ - icon: mdiRaw, - title: 'RAW file formats', - description: 'Support for RAW file formats.', - release: 'v1.61.0', - }), - withRelease({ - icon: mdiShareAll, - iconColor: 'darkturquoise', - title: 'Partner sharing (mobile)', - description: 'View shared partner photos in the mobile app.', - release: 'v1.58.0', - }), - withRelease({ - icon: mdiFile, - iconColor: 'lightblue', - title: 'XMP sidecar', - description: 'Attach XMP sidecar files to assets.', - release: 'v1.58.0', - }), - withRelease({ - icon: mdiFolder, - iconColor: 'brown', - title: 'Custom storage label', - description: 'Replace the user UUID in the storage template with a custom label.', - release: 'v1.57.0', - }), - withRelease({ - icon: mdiShareCircle, - title: 'Partner sharing', - description: 'Share your entire collection with another user.', - release: 'v1.56.0', - }), - withRelease({ - icon: mdiFaceMan, - iconColor: 'mistyrose', - title: 'Facial recognition', - description: 'Detect faces in pictures and cluster them together as people, which can be named.', - release: 'v1.56.0', - }), - withRelease({ - icon: mdiMap, - iconColor: 'darksalmon', - title: 'Map view (web)', - description: 'View a global map, with clusters of photos based on corresponding GPS data.', - release: 'v1.55.0', - }), - withRelease({ - icon: mdiDevices, - iconColor: 'slategray', - title: 'Manage auth devices', - description: 'Manage logged-in devices and revoke access from User Settings.', - release: 'v1.55.0', - }), - withRelease({ - icon: mdiStar, - iconColor: 'gold', - title: '10,000 Stars', - description: 'Reached 10K stars on GitHub!', - release: 'v1.54.0', - }), - withRelease({ - icon: mdiText, - title: 'Asset descriptions', - description: 'Save an asset description', - release: 'v1.54.0', - }), - withRelease({ - icon: mdiArchiveOutline, - title: 'Archiving', - description: 'Remove assets from the main timeline by archiving them.', - release: 'v1.54.0', - }), - withRelease({ - icon: mdiDevices, - iconColor: 'slategray', - title: 'Responsive web app', - description: 'Optimize the web app for small screen.', - release: 'v1.54.0', - }), - withRelease({ - icon: mdiFileSearch, - iconColor: 'brown', - title: 'Search by metadata', - description: 'Search images by filename, description, tagged people, make, model, and other metadata.', - release: 'v1.52.0', - }), - withRelease({ - icon: mdiImageSearch, - iconColor: 'rebeccapurple', - title: 'CLIP search', - description: 'Search images with free-form text like "Sunset at the beach".', - release: 'v1.51.0', - }), - withRelease({ - icon: mdiMagnify, - iconColor: 'lightblue', - title: 'Explore page', - description: 'View tagged places, object, and people.', - release: 'v1.51.0', - }), - withRelease({ - icon: mdiAppleIos, - title: 'iOS background uploads', - description: 'Automatically backup pictures in the background on iOS.', - release: 'v1.48.0', - }), - withRelease({ - icon: mdiMotionPlayOutline, - title: 'Auto-Link live photos', - description: 'Automatically link live photos, even when uploaded as separate files.', - release: 'v1.48.0', - }), - withRelease({ - icon: mdiMaterialDesign, - iconColor: 'blue', - title: 'Material design 3 (mobile)', - description: 'Upgrade the mobile app to Material Design 3.', - release: 'v1.47.0', - }), - withRelease({ - icon: mdiHeart, - iconColor: 'red', - title: 'Favorites (mobile)', - description: 'Show favorites on the mobile app.', - release: 'v1.46.0', - }), - withRelease({ - icon: mdiCakeVariant, - iconColor: 'deeppink', - title: 'Immich turns 1', - description: 'Immich is officially one year old.', - release: 'v1.43.0', - }), - withRelease({ - icon: mdiHeart, - iconColor: 'red', - title: 'Favorites page (web)', - description: 'Favorite and view favorites on the web.', - release: 'v1.43.0', - }), - withRelease({ - icon: mdiShareCircle, - title: 'Public share links', - description: 'Share photos and albums publicly via a shared link.', - release: 'v1.41.0', - }), - withRelease({ - icon: mdiFolder, - iconColor: 'lightblue', - title: 'User-defined storage structure', - description: 'Support custom storage structures.', - release: 'v1.39.0', - }), - withRelease({ - icon: mdiMotionPlayOutline, - title: 'iOS live photos', - description: 'Backup and display iOS Live Photos.', - release: 'v1.36.0', - }), - withRelease({ - icon: mdiSecurity, - iconColor: 'green', - title: 'OAuth integration', - description: 'Support OAuth2 and OIDC capable identity providers.', - release: 'v1.36.0', - }), - withRelease({ - icon: mdiWeb, - iconColor: 'royalblue', - title: 'Documentation site', - description: 'Release an official documentation website.', - release: 'v1.33.1', - }), - withRelease({ - icon: mdiThemeLightDark, - iconColor: 'slategray', - title: 'Dark mode (web)', - description: 'Dark mode on the web.', - release: 'v1.32.0', - }), - withRelease({ - icon: mdiPanVertical, - title: 'Virtual scrollbar (web)', - description: 'View the main timeline with a virtual scrollbar, allowing to jump to any point in time, instantly.', - release: 'v1.27.0', - }), - withRelease({ - icon: mdiCheckAll, - iconColor: 'green', - title: 'Checksum duplication check', - description: 'Enforce per user sha1 checksum uniqueness.', - release: 'v1.27.0', - }), - withRelease({ - icon: mdiAndroid, - iconColor: 'greenyellow', - title: 'Android background backup', - description: 'Automatic backup in the background on Android.', - release: 'v1.24.0', - }), - withRelease({ - icon: mdiAccountGroup, - iconColor: 'gray', - title: 'Admin portal', - description: 'Manage users and admin settings from the web.', - release: 'v1.10.0', - }), - withRelease({ - icon: mdiShareCircle, - title: 'Album sharing', - description: 'Share albums with other users.', - release: 'v1.7.0', - }), - withRelease({ - icon: mdiTag, - iconColor: 'coral', - title: 'Image tagging', - description: 'Tag images with custom values.', - release: 'v1.7.0', - }), - withRelease({ - icon: mdiImage, - iconColor: 'rebeccapurple', - title: 'View exif', - description: 'View metadata about assets.', - release: 'v1.3.0', - }), - withRelease({ - icon: mdiCheckboxMarked, - iconColor: 'green', - title: 'Multi select', - description: 'Select and execute actions on multiple assets at the same time.', - release: 'v1.2.0', - }), - withRelease({ - icon: mdiVideo, - iconColor: 'slategray', - title: 'Video player', - description: 'Play videos in the web and on mobile.', - release: 'v1.2.0', - }), - { - icon: mdiPartyPopper, - iconColor: 'deeppink', - title: 'First commit', - description: 'First commit on GitHub, Immich is born.', - getDateLabel: withLanguage(new Date(2022, 1, 3)), - }, -]; - -export default function MilestonePage(): JSX.Element { - return ( - -
-

- {title} -

-

{description}

-
- -
-
-
- ); -} diff --git a/docs/static/.well-known/security.txt b/docs/static/.well-known/security.txt deleted file mode 100644 index 5a8414c3e2..0000000000 --- a/docs/static/.well-known/security.txt +++ /dev/null @@ -1,5 +0,0 @@ -Policy: https://github.com/immich-app/immich/blob/main/SECURITY.md -Contact: mailto:security@immich.app -Preferred-Languages: en -Expires: 2026-05-01T23:59:00.000Z -Canonical: https://immich.app/.well-known/security.txt diff --git a/docs/static/_redirects b/docs/static/_redirects index 7b01d1e3bb..ecbdf19303 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -1,34 +1,34 @@ -/docs /docs/overview/welcome 307 -/docs/ /docs/overview/welcome 307 -/docs/mobile-app-beta-program /docs/features/mobile-app 307 -/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307 -/docs/install /docs/install/docker-compose 307 -/docs/installation/one-step-installation /docs/install/script 307 -/docs/installation/portainer-installation /docs/install/portainer 307 -/docs/installation/recommended-installation /docs/install/docker-compose 307 -/docs/installation/unraid /docs/install/unraid 307 -/docs/installation/requirements /docs/install/requirements 307 -/docs/overview/logo-meaning /docs/overview/logo 307 -/docs/overview/technology-stack /docs/developer/architecture 307 -/docs/usage/automatic-backup /docs/features/automatic-backup 307 -/docs/usage/bulk-upload /docs/features/command-line-interface 307 -/docs/features/bulk-upload /docs/features/command-line-interface 307 -/docs/usage/oauth /docs/administration/oauth 307 -/docs/usage/post-installation /docs/install/post-install 307 -/docs/usage/update /docs/install/docker-compose#step-4---upgrading 307 -/docs/usage/server-commands /docs/administration/server-commands 307 -/docs/features/jobs /docs/administration/jobs 307 -/docs/features/oauth /docs/administration/oauth 307 -/docs/features/password-login /docs/administration/password-login 307 -/docs/features/server-commands /docs/administration/server-commands 307 -/docs/features/storage-template /docs/administration/storage-template 307 -/docs/features/user-management /docs/administration/user-management 307 -/docs/developer/contributing /docs/developer/pr-checklist 307 -/docs/guides/machine-learning /docs/guides/remote-machine-learning 307 -/docs/administration/password-login /docs/administration/system-settings 307 -/docs/features/search /docs/features/searching 307 -/docs/features/smart-search /docs/features/searching 307 -/docs/guides/api-album-sync /docs/community-projects 307 -/docs/guides/remove-offline-files /docs/community-projects 307 -/milestones /roadmap 307 -/docs/overview/introduction /docs/overview/welcome 307 +/ /overview/quick-start 307 +/mobile-app-beta-program /features/mobile-app 307 +/contribution-guidelines /overview/support-the-project#contributing 307 +/install /install/docker-compose 307 +/installation/one-step-installation /install/script 307 +/installation/portainer-installation /install/portainer 307 +/installation/recommended-installation /install/docker-compose 307 +/installation/unraid /install/unraid 307 +/installation/requirements /install/requirements 307 +/overview/logo-meaning /overview/logo 307 +/overview/technology-stack /developer/architecture 307 +/usage/automatic-backup /features/automatic-backup 307 +/usage/bulk-upload /features/command-line-interface 307 +/features/bulk-upload /features/command-line-interface 307 +/usage/oauth /administration/oauth 307 +/usage/post-installation /install/post-install 307 +/usage/update /install/docker-compose#step-4---upgrading 307 +/usage/server-commands /administration/server-commands 307 +/features/jobs /administration/jobs 307 +/features/oauth /administration/oauth 307 +/features/password-login /administration/password-login 307 +/features/server-commands /administration/server-commands 307 +/features/storage-template /administration/storage-template 307 +/features/user-management /administration/user-management 307 +/developer/contributing /developer/pr-checklist 307 +/guides/machine-learning /guides/remote-machine-learning 307 +/administration/password-login /administration/system-settings 307 +/features/search /features/searching 307 +/features/smart-search /features/searching 307 +/guides/api-album-sync /community-projects 307 +/guides/remove-offline-files /community-projects 307 +/overview/introduction /overview/quick-start 307 +/overview/welcome /overview/quick-start 307 +/docs/* /:splat 307 diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index b884b358f0..61e6fa2a67 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,202 +1,233 @@ [ + { + "label": "v2.0.0", + "url": "https://docs.v2.0.0.archive.immich.app" + }, + { + "label": "v1.144.1", + "url": "https://docs.v1.144.1.archive.immich.app" + }, + { + "label": "v1.144.0", + "url": "https://docs.v1.144.0.archive.immich.app" + }, + { + "label": "v1.143.1", + "url": "https://docs.v1.143.1.archive.immich.app" + }, + { + "label": "v1.142.1", + "url": "https://v1.142.1.archive.immich.app", + "rootPath": "/docs" + }, + { + "label": "v1.141.1", + "url": "https://v1.141.1.archive.immich.app", + "rootPath": "/docs" + }, + { + "label": "v1.140.1", + "url": "https://v1.140.1.archive.immich.app", + "rootPath": "/docs" + }, { "label": "v1.139.4", - "url": "https://v1.139.4.archive.immich.app" - }, - { - "label": "v1.139.3", - "url": "https://v1.139.3.archive.immich.app" - }, - { - "label": "v1.139.2", - "url": "https://v1.139.2.archive.immich.app" + "url": "https://v1.139.4.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.138.1", - "url": "https://v1.138.1.archive.immich.app" - }, - { - "label": "v1.138.0", - "url": "https://v1.138.0.archive.immich.app" + "url": "https://v1.138.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.137.3", - "url": "https://v1.137.3.archive.immich.app" - }, - { - "label": "v1.137.2", - "url": "https://v1.137.2.archive.immich.app" - }, - { - "label": "v1.137.1", - "url": "https://v1.137.1.archive.immich.app" - }, - { - "label": "v1.137.0", - "url": "https://v1.137.0.archive.immich.app" + "url": "https://v1.137.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.136.0", - "url": "https://v1.136.0.archive.immich.app" + "url": "https://v1.136.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.135.3", - "url": "https://v1.135.3.archive.immich.app" - }, - { - "label": "v1.135.2", - "url": "https://v1.135.2.archive.immich.app" - }, - { - "label": "v1.135.1", - "url": "https://v1.135.1.archive.immich.app" - }, - { - "label": "v1.135.0", - "url": "https://v1.135.0.archive.immich.app" + "url": "https://v1.135.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.134.0", - "url": "https://v1.134.0.archive.immich.app" + "url": "https://v1.134.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.133.1", - "url": "https://v1.133.1.archive.immich.app" - }, - { - "label": "v1.133.0", - "url": "https://v1.133.0.archive.immich.app" + "url": "https://v1.133.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.132.3", - "url": "https://v1.132.3.archive.immich.app" + "url": "https://v1.132.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.131.3", - "url": "https://v1.131.3.archive.immich.app" + "url": "https://v1.131.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.130.3", - "url": "https://v1.130.3.archive.immich.app" + "url": "https://v1.130.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.129.0", - "url": "https://v1.129.0.archive.immich.app" + "url": "https://v1.129.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.128.0", - "url": "https://v1.128.0.archive.immich.app" + "url": "https://v1.128.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.127.0", - "url": "https://v1.127.0.archive.immich.app" + "url": "https://v1.127.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.126.1", - "url": "https://v1.126.1.archive.immich.app" + "url": "https://v1.126.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.125.7", - "url": "https://v1.125.7.archive.immich.app" + "url": "https://v1.125.7.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.124.2", - "url": "https://v1.124.2.archive.immich.app" + "url": "https://v1.124.2.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.123.0", - "url": "https://v1.123.0.archive.immich.app" + "url": "https://v1.123.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.122.3", - "url": "https://v1.122.3.archive.immich.app" + "url": "https://v1.122.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.121.0", - "url": "https://v1.121.0.archive.immich.app" + "url": "https://v1.121.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.120.2", - "url": "https://v1.120.2.archive.immich.app" + "url": "https://v1.120.2.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.119.1", - "url": "https://v1.119.1.archive.immich.app" + "url": "https://v1.119.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.118.2", - "url": "https://v1.118.2.archive.immich.app" + "url": "https://v1.118.2.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.117.0", - "url": "https://v1.117.0.archive.immich.app" + "url": "https://v1.117.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.116.2", - "url": "https://v1.116.2.archive.immich.app" + "url": "https://v1.116.2.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.115.0", - "url": "https://v1.115.0.archive.immich.app" + "url": "https://v1.115.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.114.0", - "url": "https://v1.114.0.archive.immich.app" + "url": "https://v1.114.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.113.1", - "url": "https://v1.113.1.archive.immich.app" + "url": "https://v1.113.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.112.1", - "url": "https://v1.112.1.archive.immich.app" + "url": "https://v1.112.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.111.0", - "url": "https://v1.111.0.archive.immich.app" + "url": "https://v1.111.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.110.0", - "url": "https://v1.110.0.archive.immich.app" + "url": "https://v1.110.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.109.2", - "url": "https://v1.109.2.archive.immich.app" + "url": "https://v1.109.2.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.108.0", - "url": "https://v1.108.0.archive.immich.app" + "url": "https://v1.108.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.107.2", - "url": "https://v1.107.2.archive.immich.app" + "url": "https://v1.107.2.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.106.4", - "url": "https://v1.106.4.archive.immich.app" + "url": "https://v1.106.4.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.105.1", - "url": "https://v1.105.1.archive.immich.app" + "url": "https://v1.105.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.104.0", - "url": "https://v1.104.0.archive.immich.app" + "url": "https://v1.104.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.103.1", - "url": "https://v1.103.1.archive.immich.app" + "url": "https://v1.103.1.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.102.3", - "url": "https://v1.102.3.archive.immich.app" + "url": "https://v1.102.3.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.101.0", - "url": "https://v1.101.0.archive.immich.app" + "url": "https://v1.101.0.archive.immich.app", + "rootPath": "/docs" }, { "label": "v1.100.0", - "url": "https://v1.100.0.archive.immich.app" + "url": "https://v1.100.0.archive.immich.app", + "rootPath": "/docs" } ] diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 983125e4ad..6aba8ff72a 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -38,7 +38,7 @@ services: image: redis:6.2-alpine@sha256:7fe72c486b910f6b1a9769c937dad5d63648ddee82e056f47417542dd40825bb database: - image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:7a4469b9484e37bf2630a60bc2f02f086dae898143b599ecc1c93f619849ef6b + image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:11ced39d65a92a54d12890ced6a26cc2003f92697d6f0d4d944b98459dba7138 command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf environment: POSTGRES_PASSWORD: postgres diff --git a/e2e/package.json b/e2e/package.json index beddd8e49d..1f9b7d197c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.139.4", + "version": "2.0.0", "description": "", "main": "index.js", "type": "module", @@ -19,19 +19,17 @@ "author": "", "license": "GNU Affero General Public License version 3", "devDependencies": { - "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.8.0", "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^22.17.1", + "@types/node": "^22.18.1", "@types/oidc-provider": "^9.0.0", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^3.0.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", @@ -45,7 +43,7 @@ "pngjs": "^7.0.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", - "sharp": "^0.34.0", + "sharp": "^0.34.3", "socket.io-client": "^4.7.4", "supertest": "^7.0.0", "typescript": "^5.3.3", @@ -54,6 +52,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "22.18.0" + "node": "22.20.0" } } diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 9c8b893075..5c30ff5cbe 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -1466,10 +1466,10 @@ describe('/asset', () => { expectedDate: '2023-04-04T04:00:00.000Z', }, { - name: 'CreateDate when DateTimeOriginal missing', + name: 'CreationDate when DateTimeOriginal missing', exifData: { - CreateDate: '2023:05:05 05:00:00', // TESTABLE - CreationDate: '2023:07:07 07:00:00', // TESTABLE + CreationDate: '2023:05:05 05:00:00', // TESTABLE + CreateDate: '2023:07:07 07:00:00', // TESTABLE GPSDateTime: '2023:10:10 10:00:00', // TESTABLE }, expectedDate: '2023-05-05T05:00:00.000Z', diff --git a/e2e/src/api/specs/partner.e2e-spec.ts b/e2e/src/api/specs/partner.e2e-spec.ts index db37791bac..9047a97055 100644 --- a/e2e/src/api/specs/partner.e2e-spec.ts +++ b/e2e/src/api/specs/partner.e2e-spec.ts @@ -23,8 +23,8 @@ describe('/partners', () => { ]); await Promise.all([ - createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }), - createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }), + createPartner({ partnerCreateDto: { sharedWithId: user2.userId } }, { headers: asBearerAuth(user1.accessToken) }), + createPartner({ partnerCreateDto: { sharedWithId: user1.userId } }, { headers: asBearerAuth(user2.accessToken) }), ]); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 3ac374f166..b33d6cb190 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -462,7 +462,8 @@ export const utils = { updateLibrary: (accessToken: string, id: string, dto: UpdateLibraryDto) => updateLibrary({ id, updateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), - createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }), + createPartner: (accessToken: string, id: string) => + createPartner({ partnerCreateDto: { sharedWithId: id } }, { headers: asBearerAuth(accessToken) }), updateMyPreferences: (accessToken: string, userPreferencesUpdateDto: UserPreferencesUpdateDto) => updateMyPreferences({ userPreferencesUpdateDto }, { headers: asBearerAuth(accessToken) }), diff --git a/i18n/af.json b/i18n/af.json index b3729903ec..fce944504b 100644 --- a/i18n/af.json +++ b/i18n/af.json @@ -14,6 +14,7 @@ "add_a_location": "Voeg 'n ligging by", "add_a_name": "Voeg 'n naam by", "add_a_title": "Voeg 'n titel by", + "add_birthday": "Voeg 'n verjaarsdag by", "add_endpoint": "Voeg Koppelvlakpunt by", "add_exclusion_pattern": "Voeg uitsgluitingspatrone by", "add_import_path": "Voeg invoerpad by", @@ -27,6 +28,8 @@ "add_to_album": "Voeg na album", "add_to_album_bottom_sheet_added": "By {album} bygevoeg", "add_to_album_bottom_sheet_already_exists": "Reeds in {album}", + "add_to_albums": "Voeg by albums", + "add_to_albums_count": "Voeg by ({count}) albums", "add_to_shared_album": "Voeg toe aan gedeelde album", "add_url": "Voeg URL by", "added_to_archive": "By argief toegevoegd", @@ -44,6 +47,11 @@ "backup_database": "Skep Datastortlêer", "backup_database_enable_description": "Aktiveer databasisrugsteun", "backup_keep_last_amount": "Aantal vorige rugsteune om te hou", + "backup_onboarding_3_description": "totale kopieë van jou data, insluitende die oorspronklikke lêers. Dit sluit in 1 kopie op 'n ander perseel en 2 kopieë om die huidige rekenaar.", + "backup_onboarding_description": "'N 3-2-1 rugsteun strategie word sterk aanbeveel om jou data veilig te hou. Hou kopieë van jou fotos/videos so wel as die Immich databasis vir 'n volledige rugsteun oplossing.", + "backup_onboarding_footer": "Vir meer inligting oor hoe om 'n rugsteun kopie van Immich te maak, gaan lees asseblief hierdie dokument.", + "backup_onboarding_parts_title": "'N 3-2-1 rugsteun sluit in:", + "backup_onboarding_title": "Rugsteun kopieë", "backup_settings": "Rugsteun instellings", "backup_settings_description": "Bestuur databasis rugsteun instellings.", "cleared_jobs": "Poste gevee vir: {job}", @@ -62,8 +70,8 @@ "duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search", "exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.", "external_library_management": "Eksterne Biblioteekbestuur", - "face_detection": "Gesig deteksie", - "face_detection_description": "Detecteer die gesigte in media deur middel van masjienleer. Vir videos word slegs die duimnaelskets oorweeg. “Herlaai” (ver)werk al die media weer. “Stel terug” verwyder boonop alle huidige gesigdata. “Onverwerk” plaas bates in die tou wat nog nie verwerk is nie. Gedekte gesigte sal ná voltooiing van Gesigdetectie vir Gesigherkenning in die tou geplaas word, om hulle in bestaande of nuwe persone te groepeer.", + "face_detection": "Gesig herkenning", + "face_detection_description": "Identifiseer die gesigte in media deur middel van masjienleer. Vir videos word slegs die duimnaelskets oorweeg. “Herlaai” (ver)werk al die media weer. “Stel terug” verwyder alle huidige gesigdata. “Onverwerk” plaas bates in die tou wat nog nie verwerk is nie. Geidentifiseerde gesigte sal ná voltooiing van Gesigidentifikasie vir Gesigherkenning in die tou geplaas word, om hulle in bestaande of nuwe persone te groepeer.", "facial_recognition_job_description": "Groepeer gesigte in mense in. Die stap is vinniger nadat Gesig Deteksie klaar is. \"Herstel\" (her-)groepeer alle gesigte. \"Vermiste\" plaas gesigte in ry wat nie 'n persoon gekoppel het nie.", "failed_job_command": "Opdrag {command} het misluk vir werk: {job}", "force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lêers kan nie herstel word nie.", @@ -93,15 +101,33 @@ "job_status": "Werkstatus", "library_created": "Biblioteek geskep: {library}", "library_deleted": "Biblioteek verwyder", - "library_import_path_description": "Spesifiseer 'n leer om in te neem. Hierdie leer, en al die sub leers, gaan geskandeer for vir prente en videos.", - "library_scanning": "Periodieke Skandering", - "library_scanning_description": "Stel periodieke skandering van biblioteek in", + "library_import_path_description": "Spesifiseer 'n leer om in te neem. Hierdie leer, en al die sub leers, gaan deursoek word vir prente en videos.", + "library_scanning": "Periodieke Soek", + "library_scanning_description": "Stel periodieke deursoek van biblioteek in", "library_scanning_enable_description": "Aktiveer periodieke biblioteekskandering", "library_settings": "Eksterne Biblioteek", + "library_settings_description": "Eksterne biblioteek verstellings", + "library_tasks_description": "Deursoek eksterne biblioteke vir nuwe of veranderde bates", + "library_watching_enable_description": "Hou eksterne biblioteke dop vir leer veranderinge", + "library_watching_settings": "Biblioteek dop hou (EKSPERIMENTEEL)", + "library_watching_settings_description": "Hou automaties dop vir veranderinge", + "logging_enable_description": "Aktifeer \"logging\"", + "logging_level_description": "Wanneer aktief, watter vlak van \"logs\" om te skep.", + "logging_settings": "\"Logs\"", + "machine_learning_clip_model": "CLIP model", + "machine_learning_duplicate_detection": "Duplikaat herkenning", + "machine_learning_duplicate_detection_enabled": "Aktifeer duplikaat herkenning", + "machine_learning_enabled": "Aktifeer masjienleer", + "machine_learning_facial_recognition": "Gesigsherkenning", + "machine_learning_facial_recognition_description": "Herken, identifiseer en groepeer gesigte in fotos", + "machine_learning_facial_recognition_model": "Gesigsherkennings model", + "machine_learning_facial_recognition_setting": "Aktifeer gesigsherkenning", + "machine_learning_max_detection_distance": "Maksimum herkennings afstand", "map_settings": "Kaart", "migration_job": "Migrasie", "oauth_settings": "OAuth", - "transcoding_acceleration_vaapi": "VAAPI" + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_preferred_hardware_device": "Verkiesde hardeware" }, "administration": "Administrasie", "advanced": "Gevorderde", diff --git a/i18n/ar.json b/i18n/ar.json index 5b6117c942..5d43f61777 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -123,6 +123,13 @@ "logging_enable_description": "تفعيل تسجيل الأحداث", "logging_level_description": "عند التفعيل، أي مستوى تسجيل سيستخدم.", "logging_settings": "تسجيل الاحداث", + "machine_learning_availability_checks": "تحقق من التوفر", + "machine_learning_availability_checks_description": "تحديد خوادم التعلم الآلي المتاحة تلقائيًا وإعطاءها الأولوية", + "machine_learning_availability_checks_enabled": "تفعيل عمليات فحص التوفر", + "machine_learning_availability_checks_interval": "فترة التحقق", + "machine_learning_availability_checks_interval_description": "الفترة الزمنية بالمللي ثانية بين عمليات فحص التوفر", + "machine_learning_availability_checks_timeout": "انتهت مدة انتظار الطلب", + "machine_learning_availability_checks_timeout_description": "مدة انتظار (بالمللي ثانية) لاختبارات توفر الخدمة", "machine_learning_clip_model": "نموذج CLIP", "machine_learning_clip_model_description": "اسم نموذج CLIP مدرجٌ هنا. يرجى ملاحظة أنه يجب إعادة تشغيل وظيفة \"البحث الذكي\" لجميع الصور بعد تغيير النموذج.", "machine_learning_duplicate_detection": "كشف التكرار", @@ -387,8 +394,6 @@ "admin_password": "كلمة سر المشرف", "administration": "الإدارة", "advanced": "متقدم", - "advanced_settings_beta_timeline_subtitle": "جرب تجربة التطبيق الجديدة", - "advanced_settings_beta_timeline_title": "الجدول الزمني التجريبي", "advanced_settings_enable_alternate_media_filter_subtitle": "استخدم هذا الخيار لتصفية الوسائط اثناء المزامنه بناء على معايير بديلة. جرب هذا الخيار فقط كان لديك مشاكل مع التطبيق بالكشف عن جميع الالبومات.", "advanced_settings_enable_alternate_media_filter_title": "[تجريبي] استخدم جهاز تصفية مزامنه البومات بديل", "advanced_settings_log_level_title": "مستوى السجل: {level}", @@ -396,6 +401,8 @@ "advanced_settings_prefer_remote_title": "تفضل الصور البعيدة", "advanced_settings_proxy_headers_subtitle": "عرف عناوين الوكيل التي يستخدمها Immich لارسال كل طلب شبكي", "advanced_settings_proxy_headers_title": "عناوين الوكيل", + "advanced_settings_readonly_mode_subtitle": "تتيح هذه الميزة وضع العرض فقط، حيث يمكن للمستخدم معاينة الصور فقط، بينما يتم تعطيل جميع الخيارات الأخرى مثل تحديد عدة صور، أو مشاركتها، أو بثها، أو حذفها. يمكن تفعيل/تعطيل وضع العرض فقط من خلال صورة المستخدم في الشاشة الرئيسية", + "advanced_settings_readonly_mode_title": "وضع القراءة فقط", "advanced_settings_self_signed_ssl_subtitle": "تخطي التحقق من شهادة SSL لخادم النقطة النهائي. مكلوب للشهادات الموقعة ذاتيا.", "advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا", "advanced_settings_sync_remote_deletions_subtitle": "حذف او استعادة تلقائي للاصول على هذا الجهاز عند تنفيذ العملية على الويب", @@ -423,6 +430,7 @@ "album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟", "album_search_not_found": "لم يتم ايجاد البوم مطابق لبحثك", "album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.", + "album_summary": "ملخص الألبوم", "album_updated": "تم تحديث الألبوم", "album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة", "album_user_left": "تم ترك {album}", @@ -461,6 +469,7 @@ "app_bar_signout_dialog_title": "خروج", "app_settings": "إعدادات التطبيق", "appears_in": "يظهر في", + "apply_count": "تطبيق ({count, number})", "archive": "الأرشيف", "archive_action_prompt": "{count} اضيف إلى الارشيف", "archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها", @@ -493,6 +502,8 @@ "asset_restored_successfully": "تم استعادة الاصل بنجاح", "asset_skipped": "تم تخطيه", "asset_skipped_in_trash": "في سلة المهملات", + "asset_trashed": "اصول محذوفة", + "asset_troubleshoot": "استكشاف مشاكل الأصول", "asset_uploaded": "تم الرفع", "asset_uploading": "جارٍ الرفع…", "asset_viewer_settings_subtitle": "إدارة إعدادات عارض المعرض الخاص بك", @@ -500,7 +511,9 @@ "assets": "المحتويات", "assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}", "assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم", + "assets_added_to_albums_count": "تمت اضافة {assetTotal, plural, one {# اصل} other {# اصول}} to {albumTotal, plural, one {# البوم} other {# البومات}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} لايمكن اضافته الى الالبوم", + "assets_cannot_be_added_to_albums": "{count, plural, one {اصل} other {اصول}} لا يمكن إضافته إلى أي من الألبومات", "assets_count": "{count, plural, one {# محتوى} other {# محتويات}}", "assets_deleted_permanently": "{count} الاص(و)ل المحذوف(ه) بشكل دائم", "assets_deleted_permanently_from_server": "{count} الاص(و)ل المحذوف(ه) بشكل دائمي من خادم Immich", @@ -517,14 +530,17 @@ "assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات", "assets_trashed_from_server": "{count} الاص(و)ل المنقولة الى سلة المهملات من خادم Immich", "assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل", + "assets_were_part_of_albums_count": "{count, plural, one {اصل هو} other {اصول هي}}بالفعل جزء من الألبومات", "authorized_devices": "الأجهزه المخولة", "automatic_endpoint_switching_subtitle": "اتصل محليا من خلال شبكه Wi-Fi عند توفرها و استخدم اتصالات بديله في الاماكن الاخرى", "automatic_endpoint_switching_title": "تبديل URL تلقائي", "autoplay_slideshow": "تشغيل تلقائي لعرض الشرائح", "back": "خلف", "back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد", + "background_backup_running_error": "يتم تشغيل النسخ الاحتياطي في الخلفية حاليًا، ولا يمكن بدء النسخ الاحتياطي اليدوي", "background_location_permission": "اذن الوصول للموقع في الخلفية", "background_location_permission_content": "للتمكن من تبديل الشبكه بالخلفية، Immich يحتاج*دائما* للحصول على موقع دقيق ليتمكن التطبيق من قرائة اسم شبكة الWi-Fi", + "background_options": "خيارات الخلفية", "backup": "نسخ احتياطي", "backup_album_selection_page_albums_device": "الالبومات على الجهاز ({count})", "backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء", @@ -532,6 +548,7 @@ "backup_album_selection_page_select_albums": "حدد الألبومات", "backup_album_selection_page_selection_info": "معلومات الاختيار", "backup_album_selection_page_total_assets": "إجمالي الأصول الفريدة", + "backup_albums_sync": "مزامنة ألبومات النسخ الاحتياطي", "backup_all": "الجميع", "backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة…", "backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة…", @@ -591,8 +608,6 @@ "backup_setting_subtitle": "ادارة اعدادات التحميل في الخلفية والمقدمة", "backup_settings_subtitle": "إدارة إعدادات التحميل", "backward": "الى الوراء", - "beta_sync": "حالة المزامنة التجريبية", - "beta_sync_subtitle": "ادارة نظام المزامنة الجديد", "biometric_auth_enabled": "المصادقة البايومترية مفعله", "biometric_locked_out": "لقد قفلت عنك المصادقة البيومترية", "biometric_no_options": "لا توجد خيارات بايومترية متوفرة", @@ -650,6 +665,8 @@ "change_pin_code": "تغيير رمز PIN", "change_your_password": "غير كلمة المرور الخاصة بك", "changed_visibility_successfully": "تم تغيير الرؤية بنجاح", + "charging": "الشحن", + "charging_requirement_mobile_backup": "يتطلب النسخ الاحتياطي في الخلفية أن يكون الجهاز قيد الشحن", "check_corrupt_asset_backup": "التحقق من وجود نسخ احتياطية فاسدة للاصول", "check_corrupt_asset_backup_button": "اجراء فحص", "check_corrupt_asset_backup_description": "قم بإجراء هذا الفحص فقط عبر شبكة Wi-Fi وبعد نسخ جميع الأصول احتياطيًا. قد يستغرق الإجراء بضع دقائق.", @@ -736,6 +753,7 @@ "create_user": "إنشاء مستخدم", "created": "تم الإنشاء", "created_at": "مخلوق", + "creating_linked_albums": "جاري إنشاء الألبومات المرتبطة...", "crop": "قص", "curated_object_page_title": "أشياء", "current_device": "الجهاز الحالي", @@ -885,7 +903,9 @@ "error": "خطأ", "error_change_sort_album": "فشل في تغيير ترتيب الألبوم", "error_delete_face": "حدث خطأ في حذف الوجه من الأصول", + "error_getting_places": "خطأ أثناء استرجاع بيانات المواقع", "error_loading_image": "حدث خطأ أثناء تحميل الصورة", + "error_loading_partners": "خطأ بتحميل بيانات الشركاء: {error}", "error_saving_image": "خطأ: {error}", "error_tag_face_bounding_box": "خطأ في وضع علامة على الوجه - لا يمكن الحصول على إحداثيات المربع المحيط", "error_title": "خطأ - حدث خللٌ ما", @@ -1050,6 +1070,7 @@ "favorites_page_no_favorites": "لم يتم العثور على الأصول المفضلة", "feature_photo_updated": "تم تحديث الصورة المميزة", "features": "الميزات", + "features_in_development": "الميزات قيد التطوير", "features_setting_description": "إدارة ميزات التطبيق", "file_name": "إسم الملف", "file_name_or_extension": "اسم الملف أو امتداده", @@ -1059,6 +1080,7 @@ "filter_people": "تصفية الاشخاص", "filter_places": "تصفية الاماكن", "find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث", + "first": "الاول", "fix_incorrect_match": "إصلاح المطابقة غير الصحيحة", "folder": "مجلد", "folder_not_found": "لم يتم العثور على المجلد", @@ -1069,12 +1091,15 @@ "gcast_enabled": "كوكل كاست", "gcast_enabled_description": "تقوم هذه الميزة بتحميل الموارد الخارجية من Google حتى تعمل.", "general": "عام", + "geolocation_instruction_location": "انقر على الاصل الذي يحتوي على إحداثيات نظام تحديد المواقع لاستخدام موقعه، أو اختر الموقع مباشرة من الخريطة", "get_help": "الحصول على المساعدة", "get_wifiname_error": "تعذر الحصول على اسم شبكة Wi-Fi. تأكد من منح الأذونات اللازمة واتصالك بشبكة Wi-Fi", "getting_started": "البدء", "go_back": "الرجوع للخلف", "go_to_folder": "اذهب إلى المجلد", "go_to_search": "اذهب إلى البحث", + "gps": "نظام تحديد المواقع", + "gps_missing": "لا يوجد نظام تحديد المواقع", "grant_permission": "منح الاذن", "group_albums_by": "تجميع الألبومات حسب...", "group_country": "مجموعة البلد", @@ -1180,6 +1205,7 @@ "language_search_hint": "البحث عن لغات...", "language_setting_description": "اختر لغتك المفضلة", "large_files": "ملفات كبيرة", + "last": "الاخير", "last_seen": "اخر ظهور", "latest_version": "احدث اصدار", "latitude": "خط العرض", @@ -1209,6 +1235,7 @@ "local": "محلّي", "local_asset_cast_failed": "غير قادر على بث أصل لم يتم تحميله إلى الخادم", "local_assets": "أُصول (ملفات) محلية", + "local_media_summary": "ملخص الملفات المحلية", "local_network": "شبكة محلية", "local_network_sheet_info": "سيتصل التطبيق بالخادم من خلال عنوان URL هذا عند استخدام شبكة Wi-Fi المحددة", "location_permission": "اذن الموقع", @@ -1220,6 +1247,7 @@ "location_picker_longitude_hint": "أدخل خط الطول هنا", "lock": "قفل", "locked_folder": "مجلد مقفول", + "log_detail_title": "تفاصيل السجل", "log_out": "تسجيل خروج", "log_out_all_devices": "تسجيل الخروج من كافة الأجهزة", "logged_in_as": "تم تسجيل الدخول باسم {user}", @@ -1250,6 +1278,7 @@ "login_password_changed_success": "تم تحديث كلمة السر بنجاح", "logout_all_device_confirmation": "هل أنت متأكد أنك تريد تسجيل الخروج من جميع الأجهزة؟", "logout_this_device_confirmation": "هل أنت متأكد أنك تريد تسجيل الخروج من هذا الجهاز؟", + "logs": "السجلات", "longitude": "خط الطول", "look": "الشكل", "loop_videos": "تكرار مقاطع الفيديو", @@ -1257,6 +1286,7 @@ "main_branch_warning": "أنت تستخدم إصداراً قيد التطوير؛ ونحن نوصي بشدة باستخدام إصدار النشر!", "main_menu": "القائمة الرئيسية", "make": "صنع", + "manage_geolocation": "إدارة الموقع", "manage_shared_links": "إدارة الروابط المشتركة", "manage_sharing_with_partners": "إدارة المشاركة مع الشركاء", "manage_the_app_settings": "إدارة إعدادات التطبيق", @@ -1291,6 +1321,7 @@ "mark_as_read": "تحديد كمقروء", "marked_all_as_read": "تم تحديد الكل كمقروء", "matches": "تطابقات", + "matching_assets": "‏الاصول المطابقة", "media_type": "نوع الوسائط", "memories": "الذكريات", "memories_all_caught_up": "كل شيء محدث", @@ -1331,6 +1362,7 @@ "name_or_nickname": "الاسم أو اللقب", "network_requirement_photos_upload": "استخدام بيانات الهاتف المحمول لعمل نسخة احتياطية للصور", "network_requirement_videos_upload": "استخدام بيانات الهاتف المحمول لعمل نسخة احتياطية لمقاطع الفيديو", + "network_requirements": "متطلبات الشبكة", "network_requirements_updated": "تم تغيير متطلبات الشبكة، يتم إعادة تعيين قائمة انتظار النسخ الاحتياطي", "networking_settings": "الشبكات", "networking_subtitle": "إدارة إعدادات نقطة الخادم النهائية", @@ -1341,6 +1373,7 @@ "new_person": "شخص جديد", "new_pin_code": "رمز PIN الجديد", "new_pin_code_subtitle": "هذه أول مرة تدخل فيها إلى المجلد المقفل. أنشئ رمزًا PIN للوصول بامان إلى هذه الصفحة", + "new_timeline": "الخط الزمني الجديد", "new_user_created": "تم إنشاء مستخدم جديد", "new_version_available": "إصدار جديد متاح", "newest_first": "الأحدث أولاً", @@ -1354,20 +1387,25 @@ "no_assets_message": "انقر لتحميل صورتك الأولى", "no_assets_to_show": "لا توجد أصول لعرضها", "no_cast_devices_found": "لم يتم ايجاد جهاز بث", + "no_checksum_local": "لا توجد بيانات تحقق متاحة - يتعذر تحميل الاصول المحلية", + "no_checksum_remote": "لا يوجد رمز تحقق متاح - يتعذر تحميل الاصل من الموقع البعيد", "no_duplicates_found": "لم يتم العثور على أي تكرارات.", "no_exif_info_available": "لا تتوفر معلومات exif", "no_explore_results_message": "قم برفع المزيد من الصور لاستكشاف مجموعتك.", "no_favorites_message": "أضف المفضلة للعثور بسرعة على أفضل الصور ومقاطع الفيديو", "no_libraries_message": "إنشاء مكتبة خارجية لعرض الصور ومقاطع الفيديو الخاصة بك", + "no_local_assets_found": "لم يتم العثور على أي اصول محلية تتطابق مع قيمة التحقق هذه", "no_locked_photos_message": "الصور والفديوهات في المجلد المقفل مخفية ولن تصهر في التصفح او البحث في مكتبتك.", "no_name": "لا اسم", "no_notifications": "لا توجد تنبيهات", "no_people_found": "لم يتم العثور على اشخاص مطابقين", "no_places": "لا أماكن", + "no_remote_assets_found": "لم يتم العثور على أي اصول بعيدة تتطابق مع رمز التحقق هذل", "no_results": "لا يوجد نتائج", "no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية", "no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك", "no_uploads_in_progress": "لا يوجد اي ملفات قيد الرفع", + "not_available": "غير متاح", "not_in_any_album": "ليست في أي ألبوم", "not_selected": "لم يختار", "note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق سمة التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل", @@ -1402,6 +1440,8 @@ "open_the_search_filters": "افتح مرشحات البحث", "options": "خيارات", "or": "أو", + "organize_into_albums": "ترتيب في ألبومات", + "organize_into_albums_description": "أضف الصور الموجودة إلى الألبومات باستخدام إعدادات النسخ المتزامن الحالية", "organize_your_library": "تنظيم مكتبتك", "original": "أصلي", "other": "أخرى", @@ -1487,6 +1527,7 @@ "port": "المنفذ", "preferences_settings_subtitle": "ادارة تفضيلات التطبيق", "preferences_settings_title": "التفضيلات", + "preparing": "قيد التحضير", "preset": "الإعداد المسبق", "preview": "معاينة", "previous": "السابق", @@ -1503,6 +1544,7 @@ "profile_drawer_client_out_of_date_minor": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار صغير.", "profile_drawer_client_server_up_to_date": "العميل والخادم محدثان", "profile_drawer_github": "Github", + "profile_drawer_readonly_mode": "تم تفعيل وضع القراءة فقط. اضغط مطولا على رمز صورة المستخدم للخروج.", "profile_drawer_server_out_of_date_major": "الخادم قديم.يرجى التحديث إلى أحدث إصدار رئيسي.", "profile_drawer_server_out_of_date_minor": "الخادم قديم.يرجى التحديث إلى أحدث إصدار صغير.", "profile_image_of_user": "صورة الملف الشخصي لـ {user}", @@ -1541,6 +1583,7 @@ "purchase_server_description_2": "حالة الداعم", "purchase_server_title": "الخادم", "purchase_settings_server_activated": "يتم إدارة مفتاح منتج الخادم من قبل مدير النظام", + "query_asset_id": "استعلام عن معرف الأصل", "queue_status": "يتم الاضافة الى قائمة انتظار النسخ الاحتياطي {count}/{total}", "rating": "تقييم نجمي", "rating_clear": "مسح التقييم", @@ -1548,6 +1591,9 @@ "rating_description": "‫‌اعرض تقييم EXIF في لوحة المعلومات", "reaction_options": "خيارات رد الفعل", "read_changelog": "قراءة سجل التغيير", + "readonly_mode_disabled": "تم تعطيل وضع القراءة فقط", + "readonly_mode_enabled": "تم تفعيل وضع القراءة فقط", + "ready_for_upload": "جاهز للرفع", "reassign": "إعادة التعيين", "reassigned_assets_to_existing_person": "تمت إعادة تعيين {count, plural, one {# الأصل} other {# الاصول}} إلى {name, select, null {شخص موجود } other {{name}}}", "reassigned_assets_to_new_person": "تمت إعادة تعيين {count, plural, one {# المحتوى} other {# المحتويات}} إلى شخص جديد", @@ -1572,6 +1618,7 @@ "regenerating_thumbnails": "جارٍ تجديد الصور المصغرة", "remote": "بعيد", "remote_assets": "الأُصول البعيدة", + "remote_media_summary": "ملخص الملفات البعيدة", "remove": "إزالة", "remove_assets_album_confirmation": "هل أنت متأكد أنك تريد إزالة {count, plural, one {# المحتوى} other {# المحتويات}} من الألبوم ؟", "remove_assets_shared_link_confirmation": "هل أنت متأكد أنك تريد إزالة {count, plural, one {# المحتوى} other {# المحتويات}} من رابط المشاركة هذا؟", @@ -1624,6 +1671,7 @@ "restore_user": "استعادة المستخدم", "restored_asset": "المحتويات المستعادة", "resume": "استئناف", + "resume_paused_jobs": "استكمال {count, plural, one {# وظيفة معلقة} other {# وظائف معلقة}}", "retry_upload": "أعد محاولة الرفع", "review_duplicates": "مراجعة التكرارات", "review_large_files": "مراجعة الملفات الكبيرة", @@ -1717,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "فشل في إنشاء ألبوم", "selected": "التحديد", "selected_count": "{count, plural, other {# محددة }}", + "selected_gps_coordinates": "إحداثيات نظام تحديد المواقع المختارة", "send_message": "‏إرسال رسالة", "send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا", "server_endpoint": "نقطة نهاية الخادم", @@ -1845,6 +1894,7 @@ "show_slideshow_transition": "إظهار انتقال عرض الشرائح", "show_supporter_badge": "شارة المؤيد", "show_supporter_badge_description": "إظهار شارة المؤيد", + "show_text_search_menu": "عرض قائمة خيارات البحث في النص", "shuffle": "خلط", "sidebar": "الشريط الجانبي", "sidebar_display_description": "عرض رابط للعرض في الشريط الجانبي", @@ -1875,6 +1925,7 @@ "stacktrace": "تتّبُع التكديس", "start": "ابدأ", "start_date": "تاريخ البدء", + "start_date_before_end_date": "يجب أن يكون تاريخ بدء الفترة قبل تاريخ نهايتها", "state": "الولاية", "status": "الحالة", "stop_casting": "ايقاف البث", @@ -1899,6 +1950,8 @@ "sync_albums_manual_subtitle": "مزامنة جميع الفديوهات والصور المرفوعة الى البومات الخزن الاحتياطي المختارة", "sync_local": "مزامنة الملفات المحلية", "sync_remote": "مزامنة الملفات البعيدة", + "sync_status": "حالة النسخ المتزامن", + "sync_status_subtitle": "عرض وإدارة نظام النسخ المتزامن", "sync_upload_album_setting_subtitle": "انشئ و ارفع صورك و فديوهاتك الالبومات المختارة في Immich", "tag": "العلامة", "tag_assets": "أصول العلامة", @@ -1936,7 +1989,9 @@ "to_change_password": "تغيير كلمة المرور", "to_favorite": "تفضيل", "to_login": "تسجيل الدخول", + "to_multi_select": "للتحديد المتعدد", "to_parent": "انتقل إلى الوالد", + "to_select": "للتحديد", "to_trash": "حذف", "toggle_settings": "الإعدادات", "total": "الإجمالي", @@ -1956,6 +2011,7 @@ "trash_page_select_assets_btn": "اختر الأصول", "trash_page_title": "سلة المهملات ({count})", "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.", + "troubleshoot": "استكشاف المشاكل", "type": "النوع", "unable_to_change_pin_code": "تفيير رمز PIN غير ممكن", "unable_to_setup_pin_code": "انشاء رمز PIN غير ممكن", @@ -1986,6 +2042,7 @@ "unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس", "untagged": "غير مُعَلَّم", "up_next": "التالي", + "update_location_action_prompt": "تحديث موقع {count} عناصر محددة على النحو التالي:", "updated_at": "تم التحديث", "updated_password": "تم تحديث كلمة المرور", "upload": "رفع", @@ -2052,6 +2109,7 @@ "view_next_asset": "عرض المحتوى التالي", "view_previous_asset": "عرض المحتوى السابق", "view_qr_code": "­عرض رمز الاستجابة السريعة", + "view_similar_photos": "عرض صور مشابهة", "view_stack": "عرض التكديس", "view_user": "عرض المستخدم", "viewer_remove_from_stack": "حذف من الكومه أو المجموعة", @@ -2070,5 +2128,6 @@ "yes": "نعم", "you_dont_have_any_shared_links": "ليس لديك أي روابط مشتركة", "your_wifi_name": "اسم شبكة Wi-Fi الخاص بك", - "zoom_image": "تكبير الصورة" + "zoom_image": "تكبير الصورة", + "zoom_to_bounds": "تكبير حتى حدود المنطقة" } diff --git a/i18n/az.json b/i18n/az.json index 19ca4aa08d..d0e97ca356 100644 --- a/i18n/az.json +++ b/i18n/az.json @@ -1,39 +1,62 @@ { - "about": "Haqqinda", + "about": "Haqqında", "account": "Hesab", "account_settings": "Hesab parametrləri", - "acknowledge": "Təsdiq et", + "acknowledge": "Aydındır", "action": "Əməliyyat", + "action_common_update": "Yenilə", "actions": "Əməliyyatlar", "active": "Aktiv", "activity": "Fəaliyyət", + "activity_changed": "Fəaliyyət {enabled, select, true {aktivdir} other {aktiv deyil}}", "add": "Əlavə et", "add_a_description": "Təsviri əlavə et", "add_a_location": "Məkan əlavə et", "add_a_name": "Ad əlavə et", "add_a_title": "Başlıq əlavə et", + "add_birthday": "Doğum günü əlavə et", + "add_endpoint": "Son nöqtə əlavə et", "add_exclusion_pattern": "İstisna nümunəsi əlavə et", "add_import_path": "Import yolunu əlavə et", - "add_location": "Məkanı əlavə et", + "add_location": "Məkan əlavə et", "add_more_users": "Daha çox istifadəçi əlavə et", "add_partner": "Partnyor əlavə et", "add_path": "Yol əlavə et", - "add_photos": "Şəkilləri əlavə et", - "add_to": "... əlavə et", + "add_photos": "Şəkillər əlavə et", + "add_tag": "Etiket əlavə et", + "add_to": "Bura əlavə et…", "add_to_album": "Albom əlavə et", + "add_to_album_bottom_sheet_added": "{album} albomuna əlavə edildi", + "add_to_album_bottom_sheet_already_exists": "Artıq {album} albomunda var", + "add_to_album_toggle": "{album} üçün seçimi dəyişin", + "add_to_albums": "Albomlara əlavə et", + "add_to_albums_count": "Albomlara əlavə et ({count})", "add_to_shared_album": "Paylaşılan alboma əlavə et", + "add_url": "URL əlavə et", "added_to_archive": "Arxivə əlavə edildi", "added_to_favorites": "Sevimlilələrə əlavə edildi", "added_to_favorites_count": "{count, number} şəkil sevimlilələrə əlavə edildi", "admin": { + "add_exclusion_pattern_description": "İstisna şablonlarını əlavə edin. *, ** və ? ilə Globbing dəstəklənir. Məs.: \"Raw\" adlanan hər hansısa bir qovluqda bütün faylları saymamaq üçün \"**/Raw/**\"-dan istifadə edin. \".tif\" ilə bitən bütün faylları saymamaq üçün \"**/*.tif\"-dən istifadə edin. Faylı mütləq yoldan istifadə etməklə saymamaq istəyirsinizsə \"/path/to/ignore/**\"-dan istifadə edin.", + "admin_user": "Admin İstifadəçi", + "asset_offline_description": "Bu xarici kitabxana varlığı diskdə artıq tapılmadı və zibil qutusuna köçürüldü. Əgər fayl kitabxana içərisində köçürülübsə, zaman şkalanızı yeni uyğun gələn varlıq üçün yoxlayın. Varlığı yenidən qaytarmaq üçün aşağıda verilmiş fayl yolunun Immich tərəfindən əlçatan olduğundan əmin olduqdan sonra kitabxananı skan edin.", "authentication_settings": "Səlahiyyətləndirmə parametrləri", "authentication_settings_description": "Şifrə, OAuth və digər səlahiyyətləndirmə parametrləri", "authentication_settings_disable_all": "Bütün giriş etmə metodlarını söndürmək istədiyinizdən əminsinizmi? Giriş etmə funksiyası tamamilə söndürüləcəkdir.", "authentication_settings_reenable": "Yenidən aktiv etmək üçün Server Əmri -ni istifadə edin.", "background_task_job": "Arxa plan tapşırıqları", - "backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et", - "backup_settings": "Ehtiyat Nüsxə Parametrləri", + "backup_database": "Verilənlər bazasının dump-ını yaradın", + "backup_database_enable_description": "Verilənlər bazasının artıq nüsxələrini aktiv et", + "backup_keep_last_amount": "Tutulması gərəkən nüsxələrin sayı", + "backup_onboarding_1_description": "buludda və ya başqa fiziki yerdə saytdan kənar surət.", + "backup_onboarding_2_description": "müxtəlif cihazlarda yerli nüsxələr. Bura əsas fayllar və həmin faylların ehtiyat lokal nüsxəsi daxildir.", + "backup_onboarding_3_description": "orijinal fayllar da daxil olmaqla məlumatlarınızın ümumi surətləri. Buraya 1 kənar nüsxə və 2 lokal nüsxə daxildir.", + "backup_onboarding_footer": "Immich-in ehtiyat nüsxəsini çıxarmaq haqqında ətraflı məlumat üçün sənədlərə müraciət edin.", + "backup_onboarding_parts_title": "3-2-1 ehtiyat nüsxəsinə aşağıdakılar daxildir:", + "backup_onboarding_title": "Ehtiyat surətlər", + "backup_settings": "Bazanın Dump Parametrləri", "backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et", + "cleared_jobs": "{job} üçün tapşırıqlar silindi", "config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub", "confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?", "confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın", @@ -84,5 +107,6 @@ "machine_learning_facial_recognition": "Üz Tanıma", "machine_learning_facial_recognition_description": "Şəkillərdəki üzləri aşkarla, tanı və qruplaşdır", "machine_learning_facial_recognition_model": "Üz tanıma modeli" - } + }, + "timeline": "Zaman şkalası" } diff --git a/i18n/be.json b/i18n/be.json index 64c5b21ff8..7298e904c1 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -28,6 +28,10 @@ "add_to_album": "Дадаць у альбом", "add_to_album_bottom_sheet_added": "Дададзена да {album}", "add_to_album_bottom_sheet_already_exists": "Ужо знаходзіцца ў {album}", + "add_to_album_bottom_sheet_some_local_assets": "Некаторыя лакальныя актывы не могуць быць дададзены ў альбом", + "add_to_album_toggle": "Пераключыць выбар для {album}", + "add_to_albums": "Дадаць у альбомы", + "add_to_albums_count": "Дадаць у альбомы ({count})", "add_to_shared_album": "Дадаць у агульны альбом", "add_url": "Дадаць URL", "added_to_archive": "Дададзена ў архіў", @@ -233,7 +237,10 @@ "asset_skipped_in_trash": "У сметніцы", "asset_uploaded": "Запампавана", "asset_uploading": "Запампоўванне…", + "assets_were_part_of_albums_count": "{count, plural, one {Актыў ужо быў} other {Актывы ужо былі}} часткай альбому", "authorized_devices": "Аўтарызаваныя прылады", + "automatic_endpoint_switching_subtitle": "Падключацца лакальна па вылучаным Wi-Fi, калі гэта магчыма, і выкарыстоўваць альтэрнатыўныя падключэння ў іншых месцах", + "automatic_endpoint_switching_title": "Аўтаматычнае пераключэнне URL", "back": "Назад", "backup_album_selection_page_albums_device": "Альбомы на прыладзе ({count})", "backup_all": "Усе", @@ -396,6 +403,15 @@ "purchase_button_buy": "Купіць", "purchase_button_buy_immich": "Купіць Immich", "purchase_button_select": "Выбраць", + "readonly_mode_disabled": "Выключаны рэжым толькі для чытання", + "readonly_mode_enabled": "Уключаны рэжым толькі для чытання", + "reassign": "Перапрызначыць", + "reassing_hint": "Прыпісаць выбраныя актывы існуючай асобе", + "recent": "Нядаўні", + "recent-albums": "Нядаўнія альбомы", + "recent_searches": "Нядаўнія пошукі", + "recently_added": "Нядаўна дададзена", + "refresh_faces": "Абнавіць твары", "remove": "Выдаліць", "remove_from_album": "Выдаліць з альбома", "remove_from_favorites": "Выдаліць з абраных", diff --git a/i18n/bg.json b/i18n/bg.json index 2ee5855f50..e78b139d7b 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -123,6 +123,13 @@ "logging_enable_description": "Включване на запис (логове)", "logging_level_description": "Когато е включено, какво ниво на записване да се използва.", "logging_settings": "Записване", + "machine_learning_availability_checks": "Проверки за наличност", + "machine_learning_availability_checks_description": "Автоматично откриване и предпочитане на налични сървъри за машинно обучение", + "machine_learning_availability_checks_enabled": "Активиране на проверки за наличност", + "machine_learning_availability_checks_interval": "Интервал на проверяване", + "machine_learning_availability_checks_interval_description": "Време в милисекунди между проверките за наличност", + "machine_learning_availability_checks_timeout": "Време за изчакване на отговор", + "machine_learning_availability_checks_timeout_description": "Време за изчакване на отговор в милисекунди при проверка на наличност", "machine_learning_clip_model": "CLIP модел", "machine_learning_clip_model_description": "Името на CLIP модела, посочен тук. Имайте предвид, че при промяна на модела трябва да стартирате отново задачата \"Интелигентно Търсене\" за всички изображения.", "machine_learning_duplicate_detection": "Откриване на дубликати", @@ -387,8 +394,6 @@ "admin_password": "Администраторска парола", "administration": "Администрация", "advanced": "Разширено", - "advanced_settings_beta_timeline_subtitle": "Опитайте новите функции на приложението", - "advanced_settings_beta_timeline_title": "Бета версия на времевата линия", "advanced_settings_enable_alternate_media_filter_subtitle": "При синхронизация, използвайте тази опция като филтър, основан на промяна на даден критерии. Опитайте само в случай, че приложението има проблем с откриване на всички албуми.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛНО] Използвай филтъра на алтернативното устройство за синхронизация на албуми", "advanced_settings_log_level_title": "Ниво на запис в дневника: {level}", @@ -396,13 +401,15 @@ "advanced_settings_prefer_remote_title": "Предпочитай изображенията на сървъра", "advanced_settings_proxy_headers_subtitle": "Дефиниране на прокси хедъри, които Immich трябва да изпраща с всяка мрежова заявка", "advanced_settings_proxy_headers_title": "Прокси хедъри", + "advanced_settings_readonly_mode_subtitle": "Активира режима \"само за четене\", при който снимките могат да бъдат разглеждани, но неща като избор на няколко изображения, споделяне, изтриване са забранени. Активиране/деактивиране на режима само за четене става от картинката-аватар на потребителя от основния екран", + "advanced_settings_readonly_mode_title": "Режим само за четене", "advanced_settings_self_signed_ssl_subtitle": "Пропуска проверката на SSL-сертификата на сървъра. Изисква се при самоподписани сертификати.", "advanced_settings_self_signed_ssl_title": "Разреши самоподписани SSL сертификати", "advanced_settings_sync_remote_deletions_subtitle": "Автоматично изтрии или възстанови обект на това устройство, когато действието е извършено през уеб-интерфейса", "advanced_settings_sync_remote_deletions_title": "Синхронизация на дистанционни изтривания [ЕКСПЕРИМЕНТАЛНО]", "advanced_settings_tile_subtitle": "Разширени потребителски настройки", "advanced_settings_troubleshooting_subtitle": "Разреши допълнителни възможности за отстраняване на проблеми", - "advanced_settings_troubleshooting_title": "Отстраняване на проблеми", + "advanced_settings_troubleshooting_title": "Отстраняванe на проблеми", "age_months": "Възраст {months, plural, one {# месец} other {# месеци}}", "age_year_months": "Възраст 1 година, {months, plural, one {# месец} other {# месеци}}", "age_years": "{years, plural, other {Година #}}", @@ -423,6 +430,7 @@ "album_remove_user_confirmation": "Сигурни ли сте, че искате да премахнете {user}?", "album_search_not_found": "Няма намерени албуми, отговарящи на търсенето ви", "album_share_no_users": "Изглежда, че сте споделили този албум с всички потребители или нямате друг потребител, с когото да го споделите.", + "album_summary": "Обобщение на албума", "album_updated": "Албумът е актуализиран", "album_updated_setting_description": "Получавайте известие по имейл, когато споделен албум има нови файлове", "album_user_left": "Напусна {album}", @@ -461,6 +469,7 @@ "app_bar_signout_dialog_title": "Излез от профила", "app_settings": "Настройки ма приложението", "appears_in": "Излиза в", + "apply_count": "Приложи ({count, number})", "archive": "Архив", "archive_action_prompt": "{count} са добавени в Архива", "archive_or_unarchive_photo": "Архивиране или деархивиране на снимка", @@ -493,6 +502,8 @@ "asset_restored_successfully": "Успешно възстановен обект", "asset_skipped": "Пропуснато", "asset_skipped_in_trash": "В кошчето", + "asset_trashed": "Обектът е изхвърлен", + "asset_troubleshoot": "Поправка на грешки с обекта", "asset_uploaded": "Качено", "asset_uploading": "Качване…", "asset_viewer_settings_subtitle": "Управление на настройките за изглед", @@ -500,7 +511,7 @@ "assets": "Елементи", "assets_added_count": "Добавено {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Добавен(и) са {count, plural, one {# актив} other {# актива}} в албума", - "assets_added_to_albums_count": "Добавени са {assetTotal} обекта в {albumTotal} албума", + "assets_added_to_albums_count": "{assetTotal, plural, one {# обект е добавен} other {# обекта са добавени}} в {albumTotal, plural, one {# албум} other {# албума}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Обекта не може да се добави} other {Обектите не може да се добавят}} в албума", "assets_cannot_be_added_to_albums": "{count, plural, one {обект не може да бъде добавен} other {обекта не могат да бъдат добавени}} в никой от албумите", "assets_count": "{count, plural, one {# актив} other {# актива}}", @@ -526,8 +537,10 @@ "autoplay_slideshow": "Автоматична смяна на слайдовете", "back": "Назад", "back_close_deselect": "Назад, затваряне или премахване на избора", + "background_backup_running_error": "Стартирано е фоново архивиране, не може да се пусне ръчно архивиране", "background_location_permission": "Разрешение за достъп до местоположението във фонов режим", "background_location_permission_content": "За да може да чете имената на Wi-Fi мрежите и да ги превключва при работа във фонов режим, Immich трябва *винаги* да има достъп до точното местоположение", + "background_options": "Опции за фоновите задачи", "backup": "Архивиране", "backup_album_selection_page_albums_device": "Албуми на устройството ({count})", "backup_album_selection_page_albums_tap": "Натисни за да включиш, двойно за да изключиш", @@ -535,6 +548,7 @@ "backup_album_selection_page_select_albums": "Избор на албуми", "backup_album_selection_page_selection_info": "Информация за избраното", "backup_album_selection_page_total_assets": "Уникални обекти общо", + "backup_albums_sync": "Синхронизиране на архивите", "backup_all": "Всичко", "backup_background_service_backup_failed_message": "Неуспешно архивиране. Нов опит…", "backup_background_service_connection_failed_message": "Неуспешно свързване към сървъра. Нов опит…", @@ -584,7 +598,7 @@ "backup_controller_page_turn_on": "Включи архивиране в активен режим", "backup_controller_page_uploading_file_info": "Инфо за архивирания файл", "backup_err_only_album": "Не може да се премахне единствения албум", - "backup_info_card_assets": "обекти", + "backup_info_card_assets": "обекта", "backup_manual_cancelled": "Отменено", "backup_manual_in_progress": "Върви архивиране. Опитай след малко", "backup_manual_success": "Успешно", @@ -594,8 +608,6 @@ "backup_setting_subtitle": "Управлявай настройките за архивиране в активен и фонов режим", "backup_settings_subtitle": "Управление на настройките за качване", "backward": "Назад", - "beta_sync": "Статус на бета синхронизацията", - "beta_sync_subtitle": "Управление на новата система за синхронизация", "biometric_auth_enabled": "Включена биометрично удостоверяване", "biometric_locked_out": "Няма достъп до биометрично удостоверяване", "biometric_no_options": "Няма биометрична автентикация", @@ -653,6 +665,8 @@ "change_pin_code": "Смени PIN кода", "change_your_password": "Променете паролата си", "changed_visibility_successfully": "Видимостта е променена успешно", + "charging": "При зареждане", + "charging_requirement_mobile_backup": "Фоново архивиране само при зареждане на устройството", "check_corrupt_asset_backup": "Провери за повредени архивни копия", "check_corrupt_asset_backup_button": "Провери", "check_corrupt_asset_backup_description": "Изпълни тази проверка само при Wi-Fi и след архивиране на всички обекти. Процедурата може да продължи няколко минути.", @@ -739,6 +753,7 @@ "create_user": "Създай потребител", "created": "Създадено", "created_at": "Създаден", + "creating_linked_albums": "Създаване на свързани албуми...", "crop": "Изрежи", "curated_object_page_title": "Неща", "current_device": "Текущо устройство", @@ -888,7 +903,9 @@ "error": "Грешка", "error_change_sort_album": "Неуспешна промяна на реда на сортиране на албум", "error_delete_face": "Грешка при изтриване на лице от актива", + "error_getting_places": "Грешка при събиране на местата", "error_loading_image": "Грешка при зареждане на изображението", + "error_loading_partners": "Грешка при зареждане на партньори: {error}", "error_saving_image": "Грешка: {error}", "error_tag_face_bounding_box": "Грешка при отбелязване на лице - неуспешно получаване на координати на рамката", "error_title": "Грешка - нещо се обърка", @@ -912,7 +929,7 @@ "error_selecting_all_assets": "Грешка при избора на всички файлове", "exclusion_pattern_already_exists": "Този модел за изключване вече съществува.", "failed_to_create_album": "Неуспешно създаване на албум", - "failed_to_create_shared_link": "Неуспешно създаване на споделена връзка", + "failed_to_create_shared_link": "Неуспешно създаване на спoделена връзка", "failed_to_edit_shared_link": "Неуспешно редактиране на споделена връзка", "failed_to_get_people": "Неуспешно зареждане на хора", "failed_to_keep_this_delete_others": "Неуспешно запазване на този обект и изтриване на останалите обекти", @@ -1053,6 +1070,7 @@ "favorites_page_no_favorites": "Не са намерени любими обекти", "feature_photo_updated": "Представителната снимка е променена", "features": "Функции", + "features_in_development": "Функции в процес на разработка", "features_setting_description": "Управление на функциите на приложението", "file_name": "Име на файла", "file_name_or_extension": "Име на файл или разширение", @@ -1073,12 +1091,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "За да работи тази функция зарежда външни ресурси от Google.", "general": "Общи", + "geolocation_instruction_location": "Изберете обект с GPS координати за да използвате тях или изберете място директно от картата", "get_help": "Помощ", "get_wifiname_error": "Неуспешно получаване името на Wi-Fi мрежата. Моля, убедете се, че са предоставени нужните разрешения на приложението и има връзка с Wi-Fi", "getting_started": "Как да започнем", "go_back": "Връщане назад", "go_to_folder": "Отиди в папката", "go_to_search": "Преминаване към търсене", + "gps": "GPS координати", + "gps_missing": "Няма GPS координати", "grant_permission": "Дай разрешение", "group_albums_by": "Групирай албум по...", "group_country": "Групирай по държава", @@ -1214,6 +1235,7 @@ "local": "Локално", "local_asset_cast_failed": "Не може да се предава обект, който още не е качен на сървъра", "local_assets": "Локални обекти", + "local_media_summary": "Обобщение на локалните медийни файлове", "local_network": "Локална мрежа", "local_network_sheet_info": "Приложението ще се свърже със сървъра на този URL, когато устройството е свързано към зададената Wi-Fi мрежа", "location_permission": "Разрешение за местоположение", @@ -1225,6 +1247,7 @@ "location_picker_longitude_hint": "Въведете географска дължина тук", "lock": "Заключи", "locked_folder": "Заключена папка", + "log_detail_title": "Подробности от дневника", "log_out": "Излизане", "log_out_all_devices": "Излизане с всички устройства", "logged_in_as": "Вписан като {user}", @@ -1255,6 +1278,7 @@ "login_password_changed_success": "Успешно обновена парола", "logout_all_device_confirmation": "Сигурни ли сте, че искате да излезете от всички устройства?", "logout_this_device_confirmation": "Сигурни ли сте, че искате да излезете от това устройство?", + "logs": "Дневник", "longitude": "Дължина", "look": "Изглед", "loop_videos": "Повтаряне на видеата", @@ -1262,6 +1286,7 @@ "main_branch_warning": "Използвате версия за разработчици, силно препоръчваме да използвате официална версия!", "main_menu": "Главно меню", "make": "Марка", + "manage_geolocation": "Управление на местоположенията", "manage_shared_links": "Управление на споделени връзки", "manage_sharing_with_partners": "Управление на споделянето с партньори", "manage_the_app_settings": "Управление на настройките на приложението", @@ -1296,6 +1321,7 @@ "mark_as_read": "Маркирай като четено", "marked_all_as_read": "Всички маркирани като прочетени", "matches": "Съвпадения", + "matching_assets": "Съвпадащи обекти", "media_type": "Вид медия", "memories": "Спомени", "memories_all_caught_up": "Това е всичко за днес", @@ -1336,6 +1362,7 @@ "name_or_nickname": "Име или прякор", "network_requirement_photos_upload": "Използвай мобилни данни за архивиране на снимки", "network_requirement_videos_upload": "Използвай мобилни данни за архивиране на видео", + "network_requirements": "Изисквания към мрежата", "network_requirements_updated": "Мрежовите настройки са променени, нулиране на опашката за архивиране", "networking_settings": "Мрежа", "networking_subtitle": "Управление на настройките за връзка със сървъра", @@ -1346,6 +1373,7 @@ "new_person": "Нов човек", "new_pin_code": "Нов PIN код", "new_pin_code_subtitle": "Това е първи достъп до заключена папка. Създайте PIN код за защитен достъп до тази страница", + "new_timeline": "Нова времева линия", "new_user_created": "Създаден нов потребител", "new_version_available": "НАЛИЧНА НОВА ВЕРСИЯ", "newest_first": "Най-новите първи", @@ -1359,20 +1387,25 @@ "no_assets_message": "КЛИКНЕТЕ, ЗА ДА КАЧИТЕ ПЪРВАТА СИ СНИМКА", "no_assets_to_show": "Няма обекти за показване", "no_cast_devices_found": "Няма намерени устройства за предаване", + "no_checksum_local": "Липсват контролни суми - не може да се получат локални обекти", + "no_checksum_remote": "Липсват контролни суми - не може да се получат обекти от сървъра", "no_duplicates_found": "Не бяха открити дубликати.", "no_exif_info_available": "Няма exif информация", "no_explore_results_message": "Качете още снимки, за да разгледате колекцията си.", "no_favorites_message": "Добавете в любими, за да намирате бързо най-добрите си снимки и видеоклипове", "no_libraries_message": "Създайте външна библиотека за да разглеждате снимки и видеоклипове", + "no_local_assets_found": "Не е намерен локален обект с такава контролна сума", "no_locked_photos_message": "Снимките и видеата в заключената папка са скрити и не се показват при разглеждане на библиотеката.", "no_name": "Без име", "no_notifications": "Няма известия", "no_people_found": "Не са намерени съответстващи хора", "no_places": "Няма места", + "no_remote_assets_found": "Не е намерен обект на сървъра с такава контролна сума", "no_results": "Няма резултати", "no_results_description": "Опитайте със синоним или по-обща ключова дума", "no_shared_albums_message": "Създайте албум, за да споделяте снимки и видеоклипове с хората в мрежата си", "no_uploads_in_progress": "Няма качване в момента", + "not_available": "Неналично", "not_in_any_album": "Не е в никой албум", "not_selected": "Не е избрано", "note_apply_storage_label_to_previously_uploaded assets": "Забележка: За да приложите етикета за съхранение към предварително качени активи, стартирайте", @@ -1407,6 +1440,8 @@ "open_the_search_filters": "Отвари филтрите за търсене", "options": "Настройки", "or": "или", + "organize_into_albums": "Organitzar per àlbums", + "organize_into_albums_description": "Posar les fotos existents dins dels àlbums fent servir la configuració de sincronització", "organize_your_library": "Организиране на вашата библиотека", "original": "оригинал", "other": "Други", @@ -1492,6 +1527,7 @@ "port": "Порт", "preferences_settings_subtitle": "Управление на предпочитанията на приложението", "preferences_settings_title": "Предпочитания", + "preparing": "Подготовка", "preset": "Шаблон", "preview": "Прегледи", "previous": "Предишно", @@ -1504,12 +1540,13 @@ "privacy": "Поверителност", "profile": "Профил", "profile_drawer_app_logs": "Дневник", - "profile_drawer_client_out_of_date_major": "Мобилното приложение е остаряло. Моля, актуализирай до най-новата основна версия.", - "profile_drawer_client_out_of_date_minor": "Мобилното приложение е остаряло. Моля, актуализирай до най-новата версия.", + "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_drawer_readonly_mode": "Режима само за четене е активиран. С дълго натискане върху картиката-аватар на потребителя ще деактивирате само за четене.", + "profile_drawer_server_out_of_date_major": "Версията на сървъра е остаряла. Моля, актуализирайте поне до последната главна версия.", + "profile_drawer_server_out_of_date_minor": "Версията на сървъра е остаряла. Моля, актуализирайте до последната версия.", "profile_image_of_user": "Профилна снимка на {user}", "profile_picture_set": "Профилната снимка е сложена.", "public_album": "Публичен албум", @@ -1546,6 +1583,7 @@ "purchase_server_description_2": "Статус на поддръжник", "purchase_server_title": "Сървър", "purchase_settings_server_activated": "Продуктовият ключ на сървъра се управлява от администратора", + "query_asset_id": "Buscar item per ID", "queue_status": "В опашка {count} от {total}", "rating": "Оценка със звезди", "rating_clear": "Изчисти оценката", @@ -1553,6 +1591,9 @@ "rating_description": "Покажи EXIF оценката в панела с информация", "reaction_options": "Избор на реакция", "read_changelog": "Прочети промените", + "readonly_mode_disabled": "Режима само за четене е деактивиран", + "readonly_mode_enabled": "Режима само за четене е активиран", + "ready_for_upload": "Готово за качване", "reassign": "Преназначаване", "reassigned_assets_to_existing_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на {name, select, null {съществуващ човек} other {{name}}}", "reassigned_assets_to_new_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на нов човек", @@ -1577,6 +1618,7 @@ "regenerating_thumbnails": "Пресъздаване на миниатюрите", "remote": "На сървъра", "remote_assets": "Обекти на сървъра", + "remote_media_summary": "Обобщение на медийните файлове на сървъра", "remove": "Премахни", "remove_assets_album_confirmation": "Сигурни ли сте, че искате да премахнете {count, plural, one {# елемент} other {# елемента}} от албума?", "remove_assets_shared_link_confirmation": "Сигурни ли сте, че искате да премахнете {count, plural, one {# елемент} other {# елемента}} от този споеделен линк?", @@ -1629,6 +1671,7 @@ "restore_user": "Възстанови потребител", "restored_asset": "Възстановен елемент", "resume": "Продължаване", + "resume_paused_jobs": "Продължи изпълнението на {count, plural, one {# задача} other {# задачи}}", "retry_upload": "Опитай качването отново", "review_duplicates": "Разгледай дубликатите", "review_large_files": "Преглед на големи файлове", @@ -1722,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Създаването на албум не бе успешно", "selected": "Избрано", "selected_count": "{count, plural, other {# избрани}}", + "selected_gps_coordinates": "Избрани GPS координати", "send_message": "Изпратете съобщение", "send_welcome_email": "Изпратете имейл за добре дошли", "server_endpoint": "Адрес на сървъра", @@ -1850,6 +1894,7 @@ "show_slideshow_transition": "Покажи прехода на слайдшоуто", "show_supporter_badge": "Значка поддръжник", "show_supporter_badge_description": "Покажи значка поддръжник", + "show_text_search_menu": "Покажи менюто за търсене на текст", "shuffle": "Разбъркване", "sidebar": "Странична лента", "sidebar_display_description": "Показване на връзка към изгледа в страничната лента", @@ -1880,6 +1925,7 @@ "stacktrace": "Следа на събраните", "start": "Старт", "start_date": "Начална дата", + "start_date_before_end_date": "Началната дата трябва да бъде преди крайната дата", "state": "Щат", "status": "Статус", "stop_casting": "Спри предаването", @@ -1904,6 +1950,8 @@ "sync_albums_manual_subtitle": "Синхронизирай всички заредени видеа и снимки в избраните архивни албуми", "sync_local": "Локална синхронизация", "sync_remote": "Синхронизация със сървъра", + "sync_status": "Състояние на синхронизацията", + "sync_status_subtitle": "Преглед и управление на системата за синхронизация", "sync_upload_album_setting_subtitle": "Създавайте и зареждайте снимки и видеа в избрани албуми в Immich", "tag": "Таг", "tag_assets": "Тагни елементи", @@ -1941,7 +1989,9 @@ "to_change_password": "Промяна на паролата", "to_favorite": "Любим", "to_login": "Вписване", + "to_multi_select": "за избор на няколко", "to_parent": "Отиди към родителския елемент", + "to_select": "за избор", "to_trash": "Кошче", "toggle_settings": "Превключване на настройките", "total": "Общо", @@ -1961,6 +2011,7 @@ "trash_page_select_assets_btn": "Избери обекти", "trash_page_title": "В коша ({count})", "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# ден} other {# дни}}.", + "troubleshoot": "Отстраняване на проблеми", "type": "Тип", "unable_to_change_pin_code": "Невъзможна промяна на PIN кода", "unable_to_setup_pin_code": "Неуспешно задаване на PIN кода", @@ -1991,6 +2042,7 @@ "unstacked_assets_count": "Разкачени {count, plural, one {# елемент} other {# елементи}}", "untagged": "Немаркирани", "up_next": "Следващ", + "update_location_action_prompt": "Обнови координатите на {count} избрани обекта с:", "updated_at": "Обновено", "updated_password": "Паролата е актуализирана", "upload": "Качване", @@ -2057,6 +2109,7 @@ "view_next_asset": "Преглед на следващия файл", "view_previous_asset": "Преглед на предишния файл", "view_qr_code": "Виж QR кода", + "view_similar_photos": "Виж подобни снимки", "view_stack": "Покажи в стек", "view_user": "Виж потребителя", "viewer_remove_from_stack": "Премахване от опашката", @@ -2075,5 +2128,6 @@ "yes": "Да", "you_dont_have_any_shared_links": "Нямате споделени връзки", "your_wifi_name": "Вашата Wi-Fi мрежа", - "zoom_image": "Увеличаване на изображението" + "zoom_image": "Увеличаване на изображението", + "zoom_to_bounds": "Приближи до събиране в границите" } diff --git a/i18n/bi.json b/i18n/bi.json index fff8196e75..58c84f95d9 100644 --- a/i18n/bi.json +++ b/i18n/bi.json @@ -14,5 +14,10 @@ "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_more_users": "Putem mor man", + "readonly_mode_enabled": "Mod blo yu no save janjem i on", + "reassigned_assets_to_new_person": "Janjem{count, plural, one {# asset} other {# assets}} blo nu man", + "reassing_hint": "janjem ol sumtin yu bin joos i go blo wan man", + "recent-albums": "album i no old tu mas", + "recent_searches": "lukabout wea i no old tu mas" } diff --git a/i18n/ca.json b/i18n/ca.json index bde20848d8..0e3d674557 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -2,7 +2,7 @@ "about": "Quant a", "account": "Compte", "account_settings": "Configuració del compte", - "acknowledge": "D'acord", + "acknowledge": "Base de coneixement", "action": "Acció", "action_common_update": "Actualitzar", "actions": "Accions", @@ -28,6 +28,10 @@ "add_to_album": "Afegir a un l'àlbum", "add_to_album_bottom_sheet_added": "Afegit a {album}", "add_to_album_bottom_sheet_already_exists": "Ja està a {album}", + "add_to_album_bottom_sheet_some_local_assets": "Alguns recursos locals no s'han pogut afegir a l'àlbum", + "add_to_album_toggle": "Commutar selecció de {album}", + "add_to_albums": "Afegir als àlbums", + "add_to_albums_count": "Afegir als àlbums ({count})", "add_to_shared_album": "Afegir a un àlbum compartit", "add_url": "Afegir URL", "added_to_archive": "Afegit als arxivats", @@ -81,10 +85,10 @@ "image_fullsize_enabled": "Activa la generació d'imatges a tamany complet", "image_fullsize_enabled_description": "Genera imatges a tamany complet per formats no compatibles amb la web. Quan \"Prefereix vista prèvia incrustada\" està activat, les vistes prèvies incrustades s'utilitzen directament sense conversió. No afecta els formats compatibles amb la web com JPEG.", "image_fullsize_quality_description": "De 1 a 100, qualitat de l'imatge a tamany complet. Un valor més alt és millor, però resulta en fitxers de major tamany.", - "image_fullsize_title": "Configuració d'imatges a tamany complet", + "image_fullsize_title": "Configuració de les imatges a tamany complet", "image_prefer_embedded_preview": "Prefereix vista prèvia incrustada", "image_prefer_embedded_preview_setting_description": "Empra vista prèvia incrustada en les fotografies RAW com a entrada per al processament d'imatge, quan sigui possible. Aquesta acció pot produir colors més acurats en algunes imatges, però la qualitat de la vista prèvia depèn de la càmera i la imatge pot tenir més artefactes de compressió.", - "image_prefer_wide_gamut": "Prefereix àmplia gamma", + "image_prefer_wide_gamut": "Prefereix la gamma àmplia", "image_prefer_wide_gamut_setting_description": "Uitlitza Display P3 per a les miniatures. Això preserva més bé la vitalitat de les imatges amb espais de color àmplis, però les imatges es poden veure diferent en aparells antics amb una versió antiga del navegador. Les imatges sRGB romandran com a sRGB per a evitar canvis de color.", "image_preview_description": "Imatge de mida mitjana amb metadades eliminades, que s'utilitza quan es visualitza un sol recurs i per a l'aprenentatge automàtic", "image_preview_quality_description": "Vista prèvia de la qualitat de l'1 al 100. Més alt és millor, però produeix fitxers més grans i pot reduir la capacitat de resposta de l'aplicació. Establir un valor baix pot afectar la qualitat de l'aprenentatge automàtic.", @@ -92,11 +96,11 @@ "image_quality": "Qualitat", "image_resolution": "Resolució", "image_resolution_description": "Les resolucions més altes poden conservar més detalls però triguen més a codificar-se, tenen mides de fitxer més grans i poden reduir la capacitat de resposta de l'aplicació.", - "image_settings": "Configuració d'imatges", + "image_settings": "Configuració de les imatges", "image_settings_description": "Gestiona la qualitat i resolució de les imatges generades", "image_thumbnail_description": "Miniatura petita amb metadades eliminades, que s'utilitza quan es visualitzen grups de fotos com la línia de temps principal", "image_thumbnail_quality_description": "Qualitat de miniatura d'1 a 100. Més alt és millor, però produeix fitxers més grans i pot reduir la capacitat de resposta de l'aplicació.", - "image_thumbnail_title": "Configuració de miniatures", + "image_thumbnail_title": "Configuració de les miniatures", "job_concurrency": "{job} simultàniament", "job_created": "Tasca creada", "job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.", @@ -120,6 +124,13 @@ "logging_enable_description": "Habilitar el registrament", "logging_level_description": "Quan està habilitat, quin nivell de registre es vol emprar.", "logging_settings": "Registre", + "machine_learning_availability_checks": "Comprovacions de disponibilitat", + "machine_learning_availability_checks_description": "Detectar i preferir automàticament els servidors d'aprenentatge automàtic disponibles", + "machine_learning_availability_checks_enabled": "Habilita les comprovacions de disponibilitat", + "machine_learning_availability_checks_interval": "Interval de comprovació", + "machine_learning_availability_checks_interval_description": "Interval en mil·lisegons entre comprovacions de disponibilitat", + "machine_learning_availability_checks_timeout": "Temps d'espera de la sol·licitud", + "machine_learning_availability_checks_timeout_description": "Temps d'espera en mil·lisegons per a les comprovacions de disponibilitat", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a aquí. Tingues en compte que has de tornar a executar la cerca intel·ligent per a totes les imatges quan es canvia de model.", "machine_learning_duplicate_detection": "Detecció de duplicats", @@ -384,8 +395,6 @@ "admin_password": "Contrasenya de l'administrador", "administration": "Administració", "advanced": "Avançat", - "advanced_settings_beta_timeline_subtitle": "Prova la nova experiència de l'aplicació", - "advanced_settings_beta_timeline_title": "Cronologia beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Feu servir aquesta opció per filtrar els continguts multimèdia durant la sincronització segons criteris alternatius. Només proveu-ho si teniu problemes amb l'aplicació per detectar tots els àlbums.", "advanced_settings_enable_alternate_media_filter_title": "Utilitza el filtre de sincronització d'àlbums de dispositius alternatius", "advanced_settings_log_level_title": "Nivell de registre: {level}", @@ -393,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Prefereix imatges remotes", "advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa", "advanced_settings_proxy_headers_title": "Capçaleres de proxy", + "advanced_settings_readonly_mode_subtitle": "Habilita el només de lectura mode on les fotos poden ser només vist, a coses els agrada seleccionant imatges múltiples, compartint, càsting, elimina és tot discapacitat. Habilita/Desactiva només de lectura via avatar d'usuari des de la pantalla major", + "advanced_settings_readonly_mode_title": "Mode de només lectura", "advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.", "advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats", "advanced_settings_sync_remote_deletions_subtitle": "Suprimeix o restaura automàticament un actiu en aquest dispositiu quan es realitzi aquesta acció al web", @@ -420,6 +431,7 @@ "album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?", "album_search_not_found": "No s'ha trobat cap àlbum que coincideixi amb la teva cerca", "album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.", + "album_summary": "Resum de l'àlbum", "album_updated": "Àlbum actualitzat", "album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous", "album_user_left": "Surt de {album}", @@ -458,6 +470,7 @@ "app_bar_signout_dialog_title": "Tanca la sessió", "app_settings": "Configuració de l'app", "appears_in": "Apareix a", + "apply_count": "Aplicar ({count, number})", "archive": "Arxiu", "archive_action_prompt": "{count} afegit a Arxiu", "archive_or_unarchive_photo": "Arxivar o desarxivar fotografia", @@ -490,6 +503,8 @@ "asset_restored_successfully": "Element recuperat correctament", "asset_skipped": "Saltat", "asset_skipped_in_trash": "A la paperera", + "asset_trashed": "Recurs a la paperera", + "asset_troubleshoot": "Diagnòstic de l'element", "asset_uploaded": "Carregat", "asset_uploading": "S'està carregant…", "asset_viewer_settings_subtitle": "Gestiona la configuració del visualitzador de la galeria", @@ -497,7 +512,9 @@ "assets": "Elements", "assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}", "assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum", + "assets_added_to_albums_count": "Afegits {assetTotal, plural, one {# recurs} other {# recursos}} a {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} no es pot afegir a l'àlbum", + "assets_cannot_be_added_to_albums": "{count, plural, one {El recurs} other {Els recursos}} no poden ser afegits a cap dels àlbums", "assets_count": "{count, plural, one {# recurs} other {# recursos}}", "assets_deleted_permanently": "{count} element(s) esborrats permanentment", "assets_deleted_permanently_from_server": "{count} element(s) esborrats permanentment del servidor d'Immich", @@ -514,14 +531,17 @@ "assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera", "assets_trashed_from_server": "{count} element(s) enviat a la paperera del servidor d'Immich", "assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum", + "assets_were_part_of_albums_count": "{count, plural, one {El recurs ja formava} other {Els recursos ja formaven}} part dels àlbums", "authorized_devices": "Dispositius autoritzats", "automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs", "automatic_endpoint_switching_title": "Canvi automàtic d'URL", "autoplay_slideshow": "Reprodueix automàticament les diapositives", "back": "Enrere", "back_close_deselect": "Tornar, tancar o anul·lar la selecció", + "background_backup_running_error": "La còpia de seguretat en segon pla s'està executant actualment, no es pot iniciar la còpia de seguretat manual", "background_location_permission": "Permís d'ubicació en segon pla", "background_location_permission_content": "Per canviar de xarxa quan s'executa en segon pla, Immich ha de *sempre* tenir accés a la ubicació precisa perquè l'aplicació pugui llegir el nom de la xarxa Wi-Fi", + "background_options": "Opcions en segon pla", "backup": "Còpia", "backup_album_selection_page_albums_device": "Àlbums al dispositiu ({count})", "backup_album_selection_page_albums_tap": "Un toc per incloure, doble toc per excloure", @@ -529,6 +549,7 @@ "backup_album_selection_page_select_albums": "Selecciona àlbums", "backup_album_selection_page_selection_info": "Informació de la selecció", "backup_album_selection_page_total_assets": "Total d'elements únics", + "backup_albums_sync": "Sincronització d'àlbums de còpia de seguretat", "backup_all": "Tots", "backup_background_service_backup_failed_message": "No s'ha pogut copiar els elements. Tornant a intentar…", "backup_background_service_connection_failed_message": "No s'ha pogut connectar al servidor. Tornant a intentar…", @@ -583,8 +604,10 @@ "backup_manual_in_progress": "La pujada ja està en curs. Torneu-ho a provar més tard", "backup_manual_success": "Èxit", "backup_manual_title": "Estat de pujada", + "backup_options": "Opcions de Còpia de Seguretat", "backup_options_page_title": "Opcions de còpia de seguretat", "backup_setting_subtitle": "Gestiona la configuració de càrrega en segon pla i en primer pla", + "backup_settings_subtitle": "Administra la configuració de pujada", "backward": "Enrere", "biometric_auth_enabled": "Autentificació biomètrica activada", "biometric_locked_out": "Esteu bloquejats fora de l'autenticació biomètrica", @@ -620,6 +643,7 @@ "cancel": "Cancel·la", "cancel_search": "Cancel·la la cerca", "canceled": "Cancel·lat", + "canceling": "Cancel·lant", "cannot_merge_people": "No es pot fusionar gent", "cannot_undo_this_action": "Aquesta acció no es pot desfer!", "cannot_update_the_description": "No es pot actualitzar la descripció", @@ -642,6 +666,8 @@ "change_pin_code": "Canviar el codi PIN", "change_your_password": "Canvia la teva contrasenya", "changed_visibility_successfully": "Visibilitat canviada amb èxit", + "charging": "Carregant", + "charging_requirement_mobile_backup": "La còpia de seguretat en segon pla requereix que el dispositiu estigui carregant", "check_corrupt_asset_backup": "Comprovar les còpies de seguretat corruptes", "check_corrupt_asset_backup_button": "Realitzar comprovació", "check_corrupt_asset_backup_description": "Executeu aquesta comprovació només mitjançant Wi-Fi i un cop s'hagi fet una còpia de seguretat de tots els actius. El procediment pot trigar uns minuts.", @@ -651,6 +677,7 @@ "clear": "Buida", "clear_all": "Neteja-ho tot", "clear_all_recent_searches": "Esborra totes les cerques recents", + "clear_file_cache": "Buida la memòria cau de fitxers", "clear_message": "Neteja el missatge", "clear_value": "Neteja el valor", "client_cert_dialog_msg_confirm": "OK", @@ -721,11 +748,13 @@ "create_new_user": "Crea un usuari nou", "create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS", "create_shared_album_page_share_select_photos": "Escull fotografies", + "create_shared_link": "Crea un enllaç compartit", "create_tag": "Crear etiqueta", "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "create_user": "Crea un usuari", "created": "Creat", "created_at": "Creat", + "creating_linked_albums": "Creant àlbums enllaçats...", "crop": "Retalla", "curated_object_page_title": "Coses", "current_device": "Dispositiu actual", @@ -733,6 +762,7 @@ "current_server_address": "Adreça actual del servidor", "custom_locale": "Localització personalitzada", "custom_locale_description": "Format de dates i números segons la llengua i regió", + "custom_url": "URL personalitzada", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Fosc", @@ -744,6 +774,7 @@ "date_of_birth_saved": "Data de naixement guardada amb èxit", "date_range": "Interval de dates", "day": "Dia", + "days": "Dies", "deduplicate_all": "Desduplica-ho tot", "deduplication_criteria_1": "Mida d'imatge en bytes", "deduplication_criteria_2": "Quantitat de dades EXIF", @@ -752,6 +783,7 @@ "default_locale": "Localització predeterminada", "default_locale_description": "Format de dates i números segons la configuració del navegador", "delete": "Esborra", + "delete_action_confirmation_message": "Segur que vols eliminar aquest recurs? Aquesta acció el mourà a la paperera del servidor, i et preguntarà si el vols eliminar localment", "delete_action_prompt": "{count} eliminats", "delete_album": "Esborra l'àlbum", "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?", @@ -766,9 +798,12 @@ "delete_key": "Suprimeix la clau", "delete_library": "Suprimeix la Llibreria", "delete_link": "Esborra l'enllaç", + "delete_local_action_prompt": "{count} eliminats localment", "delete_local_dialog_ok_backed_up_only": "Esborrar només les que tinguin còpia de seguretat", "delete_local_dialog_ok_force": "Suprimeix de totes maneres", "delete_others": "Suprimeix altres", + "delete_permanently": "Eliminar permanentment", + "delete_permanently_action_prompt": "{count} eliminats permanentment", "delete_shared_link": "Odstranit sdílený odkaz", "delete_shared_link_dialog_title": "Suprimeix l'enllaç compartit", "delete_tag": "Eliminar etiqueta", @@ -779,6 +814,7 @@ "description": "Descripció", "description_input_hint_text": "Afegeix descripció...", "description_input_submit_error": "S'ha produït un error en actualitzar la descripció, comproveu el registre per a més detalls", + "deselect_all": "Deseleccionar Tots", "details": "Detalls", "direction": "Direcció", "disabled": "Desactivat", @@ -796,6 +832,7 @@ "documentation": "Documentació", "done": "Fet", "download": "Descarregar", + "download_action_prompt": "Baixant {count} recursos", "download_canceled": "Descàrrega cancel·lada", "download_complete": "Descàrrega completada", "download_enqueue": "Descàrrega en cua", @@ -822,8 +859,12 @@ "edit": "Editar", "edit_album": "Edita l'àlbum", "edit_avatar": "Edita l'avatar", + "edit_birthday": "Editar aniversari", "edit_date": "Edita la data", "edit_date_and_time": "Edita data i hora", + "edit_date_and_time_action_prompt": "{count} dates i hores editades", + "edit_date_and_time_by_offset": "Canviar data mitjançant diferència", + "edit_date_and_time_by_offset_interval": "Nou rang de dates: {from}-{to}", "edit_description": "Edita la descripció", "edit_description_prompt": "Si us plau, selecciona una nova descripció:", "edit_exclusion_pattern": "Edita patró d'exclusió", @@ -852,6 +893,7 @@ "empty_trash": "Buidar la paperera", "empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!", "enable": "Activar", + "enable_backup": "Còpia de Seguretat", "enable_biometric_auth_description": "Introduïu el codi PIN per a habilitar l'autenticació biomètrica", "enabled": "Activat", "end_date": "Data final", @@ -862,7 +904,9 @@ "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_getting_places": "S'ha produït un error en obtenir els llocs", "error_loading_image": "Error carregant la imatge", + "error_loading_partners": "No s'han pogut carregar les parelles: {error}", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error a l'etiquetar la cara - no s'han pogut obtenir les coordenades de l'àrea", "error_title": "Error - Quelcom ha anat malament", @@ -895,6 +939,7 @@ "failed_to_load_notifications": "Error en carregar les notificacions", "failed_to_load_people": "No s'han pogut carregar les persones", "failed_to_remove_product_key": "No s'ha pogut eliminar la clau del producte", + "failed_to_reset_pin_code": "No s'ha pogut reiniciar el codi PIN", "failed_to_stack_assets": "No s'han pogut apilar els elements", "failed_to_unstack_assets": "No s'han pogut desapilar els elements", "failed_to_update_notification_status": "Error en actualitzar l'estat de les notificacions", @@ -903,6 +948,7 @@ "paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar", "profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.", "quota_higher_than_disk_size": "Heu establert una quota més gran que la mida de disc", + "something_went_wrong": "Alguna cosa ha anat malament", "unable_to_add_album_users": "No es poden afegir usuaris a l'àlbum", "unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit", "unable_to_add_comment": "No es pot afegir el comentari", @@ -988,6 +1034,7 @@ }, "exif": "EXIF", "exif_bottom_sheet_description": "Afegeix descripció...", + "exif_bottom_sheet_description_error": "No s'ha pogut actualitzar la descripció", "exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_location": "UBICACIÓ", "exif_bottom_sheet_people": "PERSONES", @@ -1005,6 +1052,8 @@ "explorer": "Explorador", "export": "Exporta", "export_as_json": "Exportar com a JSON", + "export_database": "Exportar base de dades", + "export_database_description": "Exportar la base de dades SQLite", "extension": "Extensió", "external": "Extern", "external_libraries": "Llibreries externes", @@ -1022,6 +1071,7 @@ "favorites_page_no_favorites": "No s'han trobat preferits", "feature_photo_updated": "Foto destacada actualitzada", "features": "Característiques", + "features_in_development": "Funcions en desenvolupament", "features_setting_description": "Administrar les funcions de l'aplicació", "file_name": "Nom de l'arxiu", "file_name_or_extension": "Nom de l'arxiu o extensió", @@ -1031,21 +1081,26 @@ "filter_people": "Filtra persones", "filter_places": "Filtrar per llocs", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", + "first": "Primer", "fix_incorrect_match": "Corregiu la coincidència incorrecta", "folder": "Carpeta", "folder_not_found": "Carpeta no trobada", "folders": "Carpetes", "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", + "forgot_pin_code_question": "Has oblidat el teu PIN?", "forward": "Endavant", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Aquesta funció carrega recursos externs de Google per funcionar.", "general": "General", + "geolocation_instruction_location": "Fes click en un element amb coordinades GPS per utilitzar la seva ubicació o selecciona una ubicació des del mapa", "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", "go_back": "Torna", "go_to_folder": "Anar al directori", "go_to_search": "Vés a cercar", + "gps": "GPS", + "gps_missing": "Sense GPS", "grant_permission": "Concedir permís", "group_albums_by": "Agrupa àlbums per...", "group_country": "Agrupar per país", @@ -1056,6 +1111,9 @@ "haptic_feedback_switch": "Activa la resposta hàptica", "haptic_feedback_title": "Resposta Hàptica", "has_quota": "Quota", + "hash_asset": "Hash del recurs", + "hashed_assets": "Recursos amb hash", + "hashing": "Generant el hash", "header_settings_add_header_tip": "Afegeix Capçalera", "header_settings_field_validator_msg": "El valor no pot estar buit", "header_settings_header_name_input": "Nom de la capçalera", @@ -1087,7 +1145,9 @@ "home_page_upload_err_limit": "Només es poden pujar un màxim de 30 elements alhora, ometent", "host": "Amfitrió", "hour": "Hora", + "hours": "Hores", "id": "ID", + "idle": "En espera", "ignore_icloud_photos": "Ignora fotos d'iCloud", "ignore_icloud_photos_description": "Les fotos emmagatzemades a iCloud no es penjaran al servidor Immich", "image": "Imatge", @@ -1145,10 +1205,13 @@ "language_no_results_title": "No s'han trobat idiomes", "language_search_hint": "Cerca idiomes...", "language_setting_description": "Seleccioneu el vostre idioma", + "large_files": "Fitxers Grans", + "last": "Últim", "last_seen": "Vist per últim cop", "latest_version": "Última versió", "latitude": "Latitud", "leave": "Marxar", + "leave_album": "Abandonar àlbum", "lens_model": "Model de lents", "let_others_respond": "Deixa que els altres responguin", "level": "Nivell", @@ -1156,11 +1219,13 @@ "library_options": "Opcions de biblioteca", "library_page_device_albums": "Àlbums al Dispositiu", "library_page_new_album": "Nou àlbum", - "library_page_sort_asset_count": "Nombre d'elements", + "library_page_sort_asset_count": "Quantitat d'elements", "library_page_sort_created": "Creat més recentment", "library_page_sort_last_modified": "Darrera modificació", "library_page_sort_title": "Títol de l'àlbum", + "licenses": "Llicències", "light": "Llum", + "like": "M'agrada", "like_deleted": "M'agrada suprimit", "link_motion_video": "Enllaçar vídeo en moviment", "link_to_oauth": "Enllaç a OAuth", @@ -1168,7 +1233,10 @@ "list": "Llista", "loading": "Carregant", "loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca", + "local": "Local", "local_asset_cast_failed": "No es pot convertir un actiu que no s'ha penjat al servidor", + "local_assets": "Recursos Locals", + "local_media_summary": "Resum de Mitjans Locals", "local_network": "Xarxa local", "local_network_sheet_info": "L'aplicació es connectarà al servidor mitjançant aquest URL quan utilitzeu la xarxa Wi-Fi especificada", "location_permission": "Permís d'ubicació", @@ -1180,6 +1248,7 @@ "location_picker_longitude_hint": "Introdueix aquí la longitud", "lock": "Bloqueja", "locked_folder": "Carpeta bloquejada", + "log_detail_title": "Detall de Registres", "log_out": "Tanca la sessió", "log_out_all_devices": "Tanqueu la sessió de tots els dispositius", "logged_in_as": "Sessió iniciada com a {user}", @@ -1191,7 +1260,7 @@ "login_form_back_button_text": "Enrere", "login_form_email_hint": "elteu@correu.cat", "login_form_endpoint_hint": "http://ip-del-servidor:port", - "login_form_endpoint_url": "URL del servidor", + "login_form_endpoint_url": "URL del punt final del servidor", "login_form_err_http": "Especifica http:// o https://", "login_form_err_invalid_email": "Adreça de correu electrònic no vàlida", "login_form_err_invalid_url": "URL no vàlid", @@ -1210,6 +1279,7 @@ "login_password_changed_success": "La contrasenya s'ha canviat correctament", "logout_all_device_confirmation": "Esteu segur que voleu tancar la sessió de tots els dispositius?", "logout_this_device_confirmation": "Esteu segur que voleu tancar la sessió d'aquest dispositiu?", + "logs": "Registres", "longitude": "Longitud", "look": "Aspecte", "loop_videos": "Vídeos en bucle", @@ -1217,6 +1287,7 @@ "main_branch_warning": "Esteu utilitzant una versió en desenvolupament; Recomanem fer servir una versió publicada!", "main_menu": "Menú principal", "make": "Fabricant", + "manage_geolocation": "Gestioneu la vostra ubicació", "manage_shared_links": "Administrar enllaços compartits", "manage_sharing_with_partners": "Gestiona la compartició amb els companys", "manage_the_app_settings": "Gestioneu la configuració de l'aplicació", @@ -1251,6 +1322,7 @@ "mark_as_read": "Marcar com ha llegit", "marked_all_as_read": "Marcat tot com a llegit", "matches": "Coincidències", + "matching_assets": "Recursos Coincidents", "media_type": "Tipus de mitjà", "memories": "Records", "memories_all_caught_up": "Posat al dia", @@ -1269,6 +1341,7 @@ "merged_people_count": "Combinades {count, plural, one {# persona} other {# persones}}", "minimize": "Minimitza", "minute": "Minut", + "minutes": "Minuts", "missing": "Restants", "model": "Model", "month": "Mes", @@ -1288,6 +1361,10 @@ "my_albums": "Els meus àlbums", "name": "Nom", "name_or_nickname": "Nom o sobrenom", + "network_requirement_photos_upload": "Fes servir dades mòbils per a còpies de seguretat de fotos", + "network_requirement_videos_upload": "Fes servir dades mòbils per a còpies de seguretat de videos", + "network_requirements": "Requeriments de Xarxa", + "network_requirements_updated": "Han canviat els requeriments de xarxa, reiniciant la cua", "networking_settings": "Xarxes", "networking_subtitle": "Gestiona la configuració del endpoint del servidor", "never": "Mai", @@ -1297,6 +1374,7 @@ "new_person": "Persona nova", "new_pin_code": "Nou codi PIN", "new_pin_code_subtitle": "Aquesta és la primera vegada que accedeixes a la carpeta bloquejada. Crea una codi PIN i accedeix de manera segura a aquesta pàgina", + "new_timeline": "Nova Línia de Temps", "new_user_created": "Nou usuari creat", "new_version_available": "NOVA VERSIÓ DISPONIBLE", "newest_first": "El més nou primer", @@ -1310,19 +1388,25 @@ "no_assets_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO", "no_assets_to_show": "No hi ha elements per mostrar", "no_cast_devices_found": "No s'han trobat dispositius per transmetre", + "no_checksum_local": "Cap checksum disponible - no s'han pogut carregar els recursos locals", + "no_checksum_remote": "Cap checksum disponible - no s'ha pogut obtenir el recurs remot", "no_duplicates_found": "No s'han trobat duplicats.", "no_exif_info_available": "No hi ha informació d'exif disponible", "no_explore_results_message": "Penja més fotos per explorar la teva col·lecció.", "no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant", "no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos", + "no_local_assets_found": "No s'ha trobat cap recurs local amb aquest checksum", "no_locked_photos_message": "Les fotos i vídeos d'aquesta carpeta estan ocultes, i no es mostraran a mesura que navegues o cerques a la teva biblioteca.", "no_name": "Sense nom", "no_notifications": "No hi ha notificacions", "no_people_found": "No s'han trobat coincidències de persones", "no_places": "No hi ha llocs", + "no_remote_assets_found": "No s'ha trobat cap recurs remot amb aquest checksum", "no_results": "Sense resultats", "no_results_description": "Proveu un sinònim o una paraula clau més general", "no_shared_albums_message": "Creeu un àlbum per compartir fotos i vídeos amb persones a la vostra xarxa", + "no_uploads_in_progress": "Cap pujada en progrés", + "not_available": "N/A", "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", @@ -1338,6 +1422,7 @@ "oauth": "OAuth", "official_immich_resources": "Recursos oficials d'Immich", "offline": "Fora de línia", + "offset": "Diferència", "ok": "D'acord", "oldest_first": "El més vell primer", "on_this_device": "En aquest dispositiu", @@ -1356,10 +1441,13 @@ "open_the_search_filters": "Obriu els filtres de cerca", "options": "Opcions", "or": "o", + "organize_into_albums": "Organitzar en àlbums", + "organize_into_albums_description": "Posar fotos existents en àlbums utilitzant la configuració de sincronització actual", "organize_your_library": "Organitzeu la llibreria", "original": "original", "other": "Altres", "other_devices": "Altres dispositius", + "other_entities": "Altres entitats", "other_variables": "Altres variables", "owned": "Propi", "owner": "Propietari", @@ -1414,6 +1502,9 @@ "permission_onboarding_permission_limited": "Permís limitat. Per a permetre que Immich faci còpies de seguretat i gestioni tota la col·lecció de la galeria, concediu permisos de fotos i vídeos a Configuració.", "permission_onboarding_request": "Immich requereix permís per veure les vostres fotos i vídeos.", "person": "Persona", + "person_age_months": "{months, plural, one {# mes} other {# mesos}} d'antiguitat", + "person_age_year_months": "1 any, {months, plural, one {# mes} other {# mesos}} d'antiguitat", + "person_age_years": "{years, plural, other {# anys}} d'antiguitat", "person_birthdate": "Nascut a {date}", "person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}", "photo_shared_all_users": "Sembla que has compartit les teves fotos amb tots els usuaris o no tens cap usuari amb qui compartir-les.", @@ -1437,6 +1528,7 @@ "port": "Port", "preferences_settings_subtitle": "Gestiona les preferències de l'aplicació", "preferences_settings_title": "Preferències", + "preparing": "Preparant", "preset": "Preestablert", "preview": "Previsualització", "previous": "Anterior", @@ -1451,8 +1543,9 @@ "profile_drawer_app_logs": "Registres", "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_client_server_up_to_date": "El client i el servidor estan actualitzats", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Mode només lectura. Feu pulsació llarga a la icona de l'avatar d'usuari per sortir.", "profile_drawer_server_out_of_date_major": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió major.", "profile_drawer_server_out_of_date_minor": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió menor.", "profile_image_of_user": "Imatge de perfil de {user}", @@ -1491,12 +1584,17 @@ "purchase_server_description_2": "Estat del contribuent", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador", + "query_asset_id": "Consulta d'identificació d'actius", + "queue_status": "En cua {count}/{total}", "rating": "Valoració", "rating_clear": "Esborrar valoració", "rating_count": "{count, plural, one {# estrella} other {# estrelles}}", "rating_description": "Mostrar la valoració EXIF al panell d'informació", "reaction_options": "Opcions de reacció", "read_changelog": "Llegeix el registre de canvis", + "readonly_mode_disabled": "Mode de només lectura desactivat", + "readonly_mode_enabled": "Mode de només lectura activat", + "ready_for_upload": "Llest per a pujar", "reassign": "Reassignar", "reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova", @@ -1519,6 +1617,9 @@ "refreshing_faces": "Refrescant cares", "refreshing_metadata": "Actualitzant les metadades", "regenerating_thumbnails": "Regenerant les miniatures", + "remote": "Remot", + "remote_assets": "Recursos Remots", + "remote_media_summary": "Resum de Mitjans Remots", "remove": "Eliminar", "remove_assets_album_confirmation": "Confirmes que vols eliminar {count, plural, one {# recurs} other {# recursos}} de l'àlbum?", "remove_assets_shared_link_confirmation": "Esteu segur que voleu eliminar {count, plural, one {# recurs} other {# recursos}} d'aquest enllaç compartit?", @@ -1526,6 +1627,7 @@ "remove_custom_date_range": "Elimina l'interval de dates personalitzat", "remove_deleted_assets": "Suprimeix fitxers fora de línia", "remove_from_album": "Treu de l'àlbum", + "remove_from_album_action_prompt": "{count} eliminats de l'àlbum", "remove_from_favorites": "Eliminar dels preferits", "remove_from_lock_folder_action_prompt": "{count} eliminades de la carpeta protegida", "remove_from_locked_folder": "Elimina de la carpeta bloquejada", @@ -1555,19 +1657,29 @@ "reset_password": "Restablir contrasenya", "reset_people_visibility": "Restablir la visibilitat de les persones", "reset_pin_code": "Restablir el codi PIN", + "reset_pin_code_description": "Si has oblidat el teu codi PIN, pots contactar amb l'administrador del servidor per a reiniciar-lo", + "reset_pin_code_success": "Codi PIN reiniciat correctament", + "reset_pin_code_with_password": "Sempre pots reiniciar el codi PIN amb la teva contrasenya", + "reset_sqlite": "Reiniciar base de dades SQLite", + "reset_sqlite_confirmation": "Segur que vols reiniciar la base de dades SQLite? Hauràs de tancar la sessió i tornar a accedir per a resincronitzar les dades", + "reset_sqlite_success": "S'ha reiniciat la base de dades correctament", "reset_to_default": "Restableix els valors predeterminats", "resolve_duplicates": "Resoldre duplicats", "resolved_all_duplicates": "Tots els duplicats resolts", "restore": "Recupera", "restore_all": "Restaurar-ho tot", + "restore_trash_action_prompt": "{count} recuperats de la paperera", "restore_user": "Restaurar l'usuari", "restored_asset": "Element restaurat", "resume": "Reprendre", + "resume_paused_jobs": "Reprèn {count, plural, one {# treball pausat} other {# treballs pausats}}", "retry_upload": "Torna a provar de pujar", "review_duplicates": "Revisar duplicats", + "review_large_files": "Revisar fitxers grans", "role": "Rol", "role_editor": "Editor", "role_viewer": "Visor", + "running": "En execució", "save": "Desa", "save_to_gallery": "Desa a galeria", "saved_api_key": "Clau d'API guardada", @@ -1654,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Error al crear l'àlbum", "selected": "Seleccionat", "selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}", + "selected_gps_coordinates": "Seleccio de coordinades GPS", "send_message": "Envia missatge", "send_welcome_email": "Envia correu de benvinguda", "server_endpoint": "Endpoint de Servidor", @@ -1699,6 +1812,7 @@ "settings_saved": "Configuració desada", "setup_pin_code": "Configurar un codi PIN", "share": "Comparteix", + "share_action_prompt": "Compartits {count} recursos", "share_add_photos": "Afegeix fotografies", "share_assets_selected": "{count} seleccionats", "share_dialog_preparing": "S'està preparant...", @@ -1720,6 +1834,7 @@ "shared_link_clipboard_copied_massage": "S'ha copiat al porta-retalls", "shared_link_clipboard_text": "Enllaç: {link}\nContrasenya: {password}", "shared_link_create_error": "S'ha produït un error en crear l'enllaç compartit", + "shared_link_custom_url_description": "Accedeix a aquest enllaç compartit amb una URL personalitzada", "shared_link_edit_description_hint": "Introduïu la descripció de compartició", "shared_link_edit_expire_after_option_day": "1 dia", "shared_link_edit_expire_after_option_days": "{count} dies", @@ -1745,6 +1860,7 @@ "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gestiona els enllaços compartits", "shared_link_options": "Opcions d'enllaços compartits", + "shared_link_password_description": "Requereix una contrasenya per accedir a aquest enllaç compartit", "shared_links": "Enllaços compartits", "shared_links_description": "Comparteix fotos i vídeos amb un enllaç", "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}", @@ -1779,6 +1895,7 @@ "show_slideshow_transition": "Mostra la transició de la presentació de diapositives", "show_supporter_badge": "Insígnia de contribuent", "show_supporter_badge_description": "Mostra una insígnia de contributor", + "show_text_search_menu": "Mostra el menú de cerca amb text", "shuffle": "Mescla", "sidebar": "Barra lateral", "sidebar_display_description": "Mostra un enllaç a la vista a la barra lateral", @@ -1792,14 +1909,16 @@ "slideshow_settings": "Configuració de diapositives", "sort_albums_by": "Ordena àlbums per...", "sort_created": "Data de creació", - "sort_items": "Nombre d'elements", + "sort_items": "Quantitat d'elements", "sort_modified": "Data de modificació", + "sort_newest": "Foto més nova", "sort_oldest": "Foto més antiga", "sort_people_by_similarity": "Ordenar personar per semblança", "sort_recent": "Foto més recent", "sort_title": "Títol", "source": "Font", "stack": "Apila", + "stack_action_prompt": "{count} apilats", "stack_duplicates": "Aplica duplicats", "stack_select_one_photo": "Selecciona una imatge principal per la pila", "stack_selected_photos": "Apila les fotos seleccionades", @@ -1807,6 +1926,7 @@ "stacktrace": "Traça de pila", "start": "Inicia", "start_date": "Data inicial", + "start_date_before_end_date": "La data d'inici ha de ser abans de la data de fi", "state": "Regió", "status": "Estat", "stop_casting": "Atura la transmisió", @@ -1819,6 +1939,7 @@ "storage_quota": "Quota d'emmagatzematge", "storage_usage": "{used} de {available} en ús", "submit": "Envia", + "success": "Amb èxit", "suggestions": "Suggeriments", "sunrise_on_the_beach": "Albada a la platja", "support": "Suport", @@ -1828,6 +1949,10 @@ "sync": "Sincronitza", "sync_albums": "Sincronitzar àlbums", "sync_albums_manual_subtitle": "Sincronitza tots els vídeos i fotos penjats amb els àlbums de còpia de seguretat seleccionats", + "sync_local": "Sincronitza Local", + "sync_remote": "Sincronitza Remot", + "sync_status": "Estat de sincronització", + "sync_status_subtitle": "Observa i administra el sistema de sincronització", "sync_upload_album_setting_subtitle": "Creeu i pugeu les seves fotos i vídeos als àlbums seleccionats a Immich", "tag": "Etiqueta", "tag_assets": "Etiquetar actius", @@ -1838,6 +1963,7 @@ "tag_updated": "Etiqueta actualizada: {tag}", "tagged_assets": "{count, plural, one {#Etiquetat} other {#Etiquetats}} {count, plural, one {# actiu} other {# actius}}", "tags": "Etiquetes", + "tap_to_run_job": "Toca per executar el treball", "template": "Plantilla", "theme": "Tema", "theme_selection": "Selecció de tema", @@ -1864,7 +1990,9 @@ "to_change_password": "Canviar la contrasenya", "to_favorite": "Prefereix", "to_login": "Iniciar sessió", + "to_multi_select": "per multi-seleccionar", "to_parent": "Anar als pares", + "to_select": "per seleccionar", "to_trash": "Paperera", "toggle_settings": "Canvia configuració", "total": "Total", @@ -1884,6 +2012,7 @@ "trash_page_select_assets_btn": "Selecciona elements", "trash_page_title": "Paperera ({count})", "trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days, plural, one {# dia} other {# dies}}.", + "troubleshoot": "Solució de problemes", "type": "Tipus", "unable_to_change_pin_code": "No es pot canviar el codi PIN", "unable_to_setup_pin_code": "No s'ha pogut configurar el codi PIN", @@ -1910,15 +2039,21 @@ "unselect_all_duplicates": "Desmarqueu tots els duplicats", "unselect_all_in": "Desseleccionar tots els elements de {group}", "unstack": "Desapila", + "unstack_action_prompt": "{count} sense apilar", "unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}", + "untagged": "Sense etiqueta", "up_next": "Pròxim", + "update_location_action_prompt": "Actualitza la ubicació de {count} elements seleccionats amb:", "updated_at": "Actualitzat", "updated_password": "Contrasenya actualitzada", "upload": "Pujar", + "upload_action_prompt": "{count} a la cua per a pujar", "upload_concurrency": "Concurrència de pujades", + "upload_details": "Detalls de la Pujada", "upload_dialog_info": "Vols fer còpia de seguretat dels elements seleccionats al servidor?", "upload_dialog_title": "Puja elements", "upload_errors": "Càrrega completada amb {count, plural, one {# error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.", + "upload_finished": "Pujada finalitzada", "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", @@ -1927,6 +2062,7 @@ "upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.", "upload_to_immich": "Puja a Immich ({count})", "uploading": "Pujant", + "uploading_media": "Pujant mitjans", "url": "URL", "usage": "Ús", "use_biometric": "Empra biometria", @@ -1947,6 +2083,7 @@ "user_usage_stats_description": "Veure les estadístiques d'ús del compte", "username": "Nom d'usuari", "users": "Usuaris", + "users_added_to_album_count": "{count, plural, one {S'ha afegit # usuari} other {S'han afegit # usuaris}} a l'àlbum", "utilities": "Utilitats", "validate": "Valida", "validate_endpoint_error": "Per favor introdueix un URL vàlid", @@ -1965,6 +2102,7 @@ "view_album": "Veure l'àlbum", "view_all": "Veure tot", "view_all_users": "Mostra tot els usuaris", + "view_details": "Veure Detalls", "view_in_timeline": "Mostrar a la línia de temps", "view_link": "Veure enllaç", "view_links": "Mostra enllaços", @@ -1972,6 +2110,7 @@ "view_next_asset": "Mostra el següent element", "view_previous_asset": "Mostra l'element anterior", "view_qr_code": "Veure codi QR", + "view_similar_photos": "Veure fotos similars", "view_stack": "Veure la pila", "view_user": "Veure Usuari", "viewer_remove_from_stack": "Elimina de la pila", @@ -1990,5 +2129,6 @@ "yes": "Sí", "you_dont_have_any_shared_links": "No tens cap enllaç compartit", "your_wifi_name": "Nom del teu Wi-Fi", - "zoom_image": "Ampliar Imatge" + "zoom_image": "Ampliar Imatge", + "zoom_to_bounds": "Amplia als límits" } diff --git a/i18n/cs.json b/i18n/cs.json index d9159eb4a4..ceebc9f401 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -28,6 +28,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_album_bottom_sheet_some_local_assets": "Některá místní aktiva nebylo možné přidat do alba", "add_to_album_toggle": "Přepnout výběr pro {album}", "add_to_albums": "Přidat do alb", "add_to_albums_count": "Přidat do alb ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Povolit protokolování", "logging_level_description": "Když je povoleno, jakou úroveň protokolu použít.", "logging_settings": "Protokolování", + "machine_learning_availability_checks": "Kontroly dostupnosti", + "machine_learning_availability_checks_description": "Automaticky zvolit a preferovat dostupné servery strojového učení", + "machine_learning_availability_checks_enabled": "Povolit kontroly dostupnosti", + "machine_learning_availability_checks_interval": "Interval kontrol", + "machine_learning_availability_checks_interval_description": "Interval v milisekundách mezi kontrolami dostupnosti", + "machine_learning_availability_checks_timeout": "Vypršení požadavku", + "machine_learning_availability_checks_timeout_description": "Časové vypršení požadavku v milisekundách u kontrol dostupnosti", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "Název CLIP modelu je uvedený zde. Pamatujte, že při změně modelu je nutné znovu spustit úlohu 'Chytré vyhledávání' pro všechny obrázky.", "machine_learning_duplicate_detection": "Kontrola duplicit", @@ -257,7 +265,7 @@ "server_settings_description": "Správa nastavení serveru", "server_welcome_message": "Uvítací zpráva", "server_welcome_message_description": "Zpráva, která se zobrazí na přihlašovací stránce.", - "sidecar_job": "Sidecar metadata", + "sidecar_job": "Postranní metadata", "sidecar_job_description": "Objevování nebo synchronizace sidecar metadat ze systému souborů", "slideshow_duration_description": "Počet sekund pro zobrazení každého obrázku", "smart_search_job_description": "Strojové učení na objektech pro podporu inteligentního vyhledávání", @@ -341,11 +349,11 @@ "transcoding_settings_description": "Správa rozlišení a kódování videosouborů", "transcoding_target_resolution": "Cílové rozlišení", "transcoding_target_resolution_description": "Vyšší rozlišení mohou zachovat více detailů, ale jejich kódování trvá déle, mají větší velikost souboru a mohou snížit odezvu aplikace.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "Časové AQ", "transcoding_temporal_aq_description": "Platí pouze pro NVENC. Zvyšuje kvalitu scén s vysokým počtem detailů a malým počtem pohybů. Nemusí být kompatibilní se staršími zařízeními.", "transcoding_threads": "Vlákna", "transcoding_threads_description": "Vyšší hodnoty vedou k rychlejšímu kódování, ale ponechávají serveru méně prostoru pro zpracování jiných úloh. Tato hodnota by neměla být vyšší než počet jader procesoru. Maximalizuje využití, pokud je nastavena na 0.", - "transcoding_tone_mapping": "Tone-mapping", + "transcoding_tone_mapping": "Mapování tónů", "transcoding_tone_mapping_description": "Snaží se zachovat vzhled videí HDR při převodu na SDR. Každý algoritmus dělá různé kompromisy v oblasti barev, detailů a jasu. Hable zachovává detaily, Mobius zachovává barvy a Reinhard zachovává jas.", "transcoding_transcode_policy": "Zásady překódování", "transcoding_transcode_policy_description": "Zásady, kdy má být video překódováno. Videa HDR budou překódována vždy (kromě případů, kdy je překódování zakázáno).", @@ -387,8 +395,6 @@ "admin_password": "Heslo správce", "administration": "Administrace", "advanced": "Pokročilé", - "advanced_settings_beta_timeline_subtitle": "Vyzkoušejte nové prostředí aplikace", - "advanced_settings_beta_timeline_title": "Beta verze časové osy", "advanced_settings_enable_alternate_media_filter_subtitle": "Tuto možnost použijte k filtrování médií během synchronizace na základě alternativních kritérií. Tuto možnost vyzkoušejte pouze v případě, že máte problémy s detekcí všech alb v aplikaci.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNÍ] Použít alternativní filtr pro synchronizaci alb zařízení", "advanced_settings_log_level_title": "Úroveň protokolování: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky", "advanced_settings_proxy_headers_subtitle": "Definice hlaviček proxy serveru, které by měl Immich odesílat s každým síťovým požadavkem", "advanced_settings_proxy_headers_title": "Proxy hlavičky", + "advanced_settings_readonly_mode_subtitle": "Povoluje režim pouze pro čtení, ve kterém lze fotografie pouze prohlížet, ale funkce jako výběr více obrázků, sdílení, přenos, mazání jsou zakázány. Povolení/zakázání režimu pouze pro čtení pomocí avatara uživatele na hlavní obrazovce", + "advanced_settings_readonly_mode_title": "Režim pouze pro čtení", "advanced_settings_self_signed_ssl_subtitle": "Vynechá ověření SSL certifikátu serveru. Vyžadováno pro self-signed certifikáty.", "advanced_settings_self_signed_ssl_title": "Povolit self-signed SSL certifikáty", "advanced_settings_sync_remote_deletions_subtitle": "Automaticky odstranit nebo obnovit položku v tomto zařízení, když je tato akce provedena na webu", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Opravdu chcete odebrat uživatele {user}?", "album_search_not_found": "Nebyla nalezena žádná alba odpovídající vašemu hledání", "album_share_no_users": "Zřejmě jste toto album sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste ho mohli sdílet.", + "album_summary": "Souhrn alba", "album_updated": "Album aktualizováno", "album_updated_setting_description": "Dostávat e-mailová oznámení o nových položkách sdíleného alba", "album_user_left": "Opustil {album}", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Odhlásit se", "app_settings": "Aplikace", "appears_in": "Vyskytuje se v", + "apply_count": "Použít ({count, number})", "archive": "Archiv", "archive_action_prompt": "{count} přidaných do archivu", "archive_or_unarchive_photo": "Přidat nebo odebrat fotku z archivu", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Položka úspěšně obnovena", "asset_skipped": "Přeskočeno", "asset_skipped_in_trash": "V koši", + "asset_trashed": "Položka vyhozena", + "asset_troubleshoot": "Řešení problémů s položkami", "asset_uploaded": "Nahráno", "asset_uploading": "Nahrávání…", "asset_viewer_settings_subtitle": "Správa nastavení prohlížeče galerie", @@ -500,7 +512,7 @@ "assets": "Položky", "assets_added_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}}", "assets_added_to_album_count": "Do alba {count, plural, one {byla přidána # položka} few {byly přidány # položky} other {bylo přidáno # položek}}", - "assets_added_to_albums_count": "{assetTotal, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}} do {albumTotal} alb", + "assets_added_to_albums_count": "{assetTotal, plural, one {Přidána # položka} few{Přidány # položky} other {Přidáno # položek}} do {albumTotal, plural, one {# alba} other {# alb}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Položku} other {Položky}} nelze přidat do alba", "assets_cannot_be_added_to_albums": "{count, plural, one {Položku} other {Položky}} nelze přidat do žádného z alb", "assets_count": "{count, plural, one {# položka} few {# položky} other {# položek}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Automatické přehrávání prezentace", "back": "Zpět", "back_close_deselect": "Zpět, zavřít nebo zrušit výběr", + "background_backup_running_error": "Právě probíhá zálohování na pozadí, nelze spustit ruční zálohování", "background_location_permission": "Povolení polohy na pozadí", "background_location_permission_content": "Aby bylo možné přepínat sítě při běhu na pozadí, musí mít Immich *vždy* přístup k přesné poloze, aby mohl zjistit název Wi-Fi sítě", + "background_options": "Možnosti běhu na pozadí", "backup": "Záloha", "backup_album_selection_page_albums_device": "Alba v zařízení ({count})", "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, opětovným klepnutím ji vyloučíte", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Vybraná alba", "backup_album_selection_page_selection_info": "Informace o výběru", "backup_album_selection_page_total_assets": "Celkový počet jedinečných položek", + "backup_albums_sync": "Synchronizace zálohovaných alb", "backup_all": "Vše", "backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu…", "backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "Správa nastavení zálohování na pozadí a na popředí", "backup_settings_subtitle": "Správa nastavení nahrávání", "backward": "Pozpátku", - "beta_sync": "Stav synchronizace (beta)", - "beta_sync_subtitle": "Správa nového systému synchronizace", "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", @@ -653,6 +666,8 @@ "change_pin_code": "Změnit PIN kód", "change_your_password": "Změna vašeho hesla", "changed_visibility_successfully": "Změna viditelnosti proběhla úspěšně", + "charging": "Nabíjení", + "charging_requirement_mobile_backup": "Zálohování na pozadí vyžaduje, aby bylo zařízení nabíjeno", "check_corrupt_asset_backup": "Kontrola poškozených záloh položek", "check_corrupt_asset_backup_button": "Provést kontrolu", "check_corrupt_asset_backup_description": "Tuto kontrolu provádějte pouze přes Wi-Fi a po zálohování všech prostředků. Takto operace může trvat několik minut.", @@ -739,6 +754,7 @@ "create_user": "Vytvořit uživatele", "created": "Vytvořeno", "created_at": "Vytvořeno", + "creating_linked_albums": "Vytváření propojených alb...", "crop": "Oříznout", "curated_object_page_title": "Věci", "current_device": "Současné zařízení", @@ -888,7 +904,9 @@ "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", + "error_getting_places": "Chyba při zjišťování míst", "error_loading_image": "Chyba při načítání obrázku", + "error_loading_partners": "Chyba při načítání partnerů: {error}", "error_saving_image": "Chyba: {error}", "error_tag_face_bounding_box": "Chyba při označování obličeje - nelze získat souřadnice ohraničujícího rámečku", "error_title": "Chyba - Něco se pokazilo", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "Nebyla nalezena žádná oblíbená média", "feature_photo_updated": "Hlavní fotka aktualizována", "features": "Funkce", + "features_in_development": "Funkce ve vývoji", "features_setting_description": "Správa funkcí aplikace", "file_name": "Název souboru", "file_name_or_extension": "Název nebo přípona souboru", @@ -1073,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Tato funkce načítá externí zdroje z Googlu, aby mohla fungovat.", "general": "Obecné", + "geolocation_instruction_location": "Klikněte na položku s GPS souřadnicemi, abyste mohli použít její polohu, nebo vyberte polohu přímo z mapy", "get_help": "Získat pomoc", "get_wifiname_error": "Nepodařilo se získat název Wi-Fi. Zkontrolujte, zda jste udělili potřebná oprávnění a zda jste připojeni k Wi-Fi síti", "getting_started": "Začínáme", "go_back": "Přejít zpět", "go_to_folder": "Přejít do složky", "go_to_search": "Přejít na vyhledávání", + "gps": "GPS", + "gps_missing": "Bez GPS", "grant_permission": "Udělit oprávnění", "group_albums_by": "Seskupit alba podle...", "group_country": "Seskupit podle země", @@ -1214,17 +1236,19 @@ "local": "Místní", "local_asset_cast_failed": "Nelze odeslat položku, která není nahraná na serveru", "local_assets": "Místní položky", + "local_media_summary": "Souhrn místních médií", "local_network": "Místní síť", "local_network_sheet_info": "Aplikace se při použití zadané sítě Wi-Fi připojí k serveru prostřednictvím tohoto URL", "location_permission": "Oprávnění polohy", "location_permission_content": "Aby bylo možné používat funkci automatického přepínání, potřebuje Immich oprávnění k přesné poloze, aby mohl přečíst název aktuální sítě Wi-Fi", - "location_picker_choose_on_map": "Vyberte na mapě", + "location_picker_choose_on_map": "Vybrat na mapě", "location_picker_latitude_error": "Zadejte platnou zeměpisnou šířku", "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_detail_title": "Podrobnosti protokolu", "log_out": "Odhlásit", "log_out_all_devices": "Odhlásit všechna zařízení", "logged_in_as": "Přihlášen jako {user}", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "Heslo bylo úspěšně aktualizováno", "logout_all_device_confirmation": "Opravdu chcete odhlásit všechna zařízení?", "logout_this_device_confirmation": "Opravdu chcete odhlásit toto zařízení?", + "logs": "Protokoly", "longitude": "Zeměpisná délka", "look": "Zobrazení", "loop_videos": "Videa ve smyčce", @@ -1262,6 +1287,7 @@ "main_branch_warning": "Používáte vývojovou verzi; důrazně doporučujeme používat verzi z vydání!", "main_menu": "Hlavní nabídka", "make": "Výrobce", + "manage_geolocation": "Spravovat polohu", "manage_shared_links": "Spravovat sdílené odkazy", "manage_sharing_with_partners": "Správa sdílení s partnery", "manage_the_app_settings": "Správa nastavení aplikace", @@ -1296,6 +1322,7 @@ "mark_as_read": "Označit jako přečtené", "marked_all_as_read": "Vše označeno jako přečtené", "matches": "Shody", + "matching_assets": "Odpovídající položky", "media_type": "Typ média", "memories": "Vzpomínky", "memories_all_caught_up": "To je všechno", @@ -1336,6 +1363,7 @@ "name_or_nickname": "Jméno nebo přezdívka", "network_requirement_photos_upload": "Pro zálohování fotografií používat mobilní data", "network_requirement_videos_upload": "Pro zálohování videí používat mobilní data", + "network_requirements": "Požadavky na síť", "network_requirements_updated": "Požadavky na síť se změnily, fronta zálohování se vytvoří znovu", "networking_settings": "Síť", "networking_subtitle": "Správa nastavení koncového bodu serveru", @@ -1346,6 +1374,7 @@ "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_timeline": "Nová časová osa", "new_user_created": "Vytvořen nový uživatel", "new_version_available": "NOVÁ VERZE K DISPOZICI", "newest_first": "Nejnovější první", @@ -1359,20 +1388,25 @@ "no_assets_message": "KLIKNĚTE PRO NAHRÁNÍ PRVNÍ FOTOGRAFIE", "no_assets_to_show": "Žádné položky k zobrazení", "no_cast_devices_found": "Nebyla nalezena žádná zařízení", + "no_checksum_local": "Není k dispozici kontrolní součet - nelze načíst místní položky", + "no_checksum_remote": "Není k dispozici kontrolní součet - nelze načíst vzdálenou položku", "no_duplicates_found": "Nebyly nalezeny žádné duplicity.", "no_exif_info_available": "Exif není k dispozici", "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_local_assets_found": "Nebyly nalezeny žádné místní položky s tímto kontrolním součtem", "no_locked_photos_message": "Fotky a videa v uzamčené složce jsou skryté a při procházení nebo vyhledávání v knihovně se nezobrazují.", "no_name": "Bez jména", "no_notifications": "Žádná oznámení", "no_people_found": "Nebyli nalezeni žádní odpovídající lidé", "no_places": "Žádná místa", + "no_remote_assets_found": "Nebyly nalezeny žádné vzdálené položky s tímto kontrolním součtem", "no_results": "Žádné výsledky", "no_results_description": "Zkuste použít synonymum nebo obecnější klíčové slovo", "no_shared_albums_message": "Vytvořte si album a sdílejte fotografie a videa s lidmi ve své síti", "no_uploads_in_progress": "Neprobíhá žádné nahrávání", + "not_available": "Není k dispozici", "not_in_any_album": "Bez alba", "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", @@ -1407,6 +1441,8 @@ "open_the_search_filters": "Otevřít vyhledávací filtry", "options": "Možnosti", "or": "nebo", + "organize_into_albums": "Organizovat do alb", + "organize_into_albums_description": "Umístit existující fotky do alb s použitím aktuálního nastavení synchronizace", "organize_your_library": "Uspořádejte si knihovnu", "original": "originál", "other": "Ostatní", @@ -1452,7 +1488,7 @@ "permanent_deletion_warning_setting_description": "Zobrazit varování při trvalém odstranění položek", "permanently_delete": "Trvale odstranit", "permanently_delete_assets_count": "Trvale smazat {count, plural, one {položku} other {položky}}", - "permanently_delete_assets_prompt": "Opravdu chcete trvale smazat {count, plural, one {tuto položku} few {tyto # položky} other {těchto # položek}}? Tím {count, plural, one {ji také odstraníte z jejích} other {je také odstraníte z jejich}} alb.", + "permanently_delete_assets_prompt": "Opravdu chcete trvale smazat {count, plural, one {tento soubor?} other {tyto # soubory?}} Tím se také odstraní {count, plural, one {z jeho} other {z jejich}} alba.", "permanently_deleted_asset": "Položka trvale odstraněna", "permanently_deleted_assets_count": "{count, plural, one {Položka trvale vymazána} other {Položky trvale vymazány}}", "permission": "Oprávnění", @@ -1492,6 +1528,7 @@ "port": "Port", "preferences_settings_subtitle": "Správa předvoleb aplikace", "preferences_settings_title": "Předvolby", + "preparing": "Příprava", "preset": "Přednastavení", "preview": "Náhled", "previous": "Předchozí", @@ -1508,6 +1545,7 @@ "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_readonly_mode": "Režim jen pro čtení. Ukončíte ho dlouhým podržením ikony avataru.", "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}", @@ -1546,6 +1584,7 @@ "purchase_server_description_2": "Stav podporovatele", "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce", + "query_asset_id": "ID položky dotazu", "queue_status": "Ve frontě {count}/{total}", "rating": "Hodnocení hvězdičkami", "rating_clear": "Vyčistit hodnocení", @@ -1553,6 +1592,9 @@ "rating_description": "Zobrazit EXIF hodnocení v informačním panelu", "reaction_options": "Možnosti reakce", "read_changelog": "Přečtěte si seznam změn", + "readonly_mode_disabled": "Režim pouze pro čtení je deaktivován", + "readonly_mode_enabled": "Režim pouze pro čtení povolen", + "ready_for_upload": "Připraveno k nahrání", "reassign": "Přeřadit", "reassigned_assets_to_existing_person": "Přeřadit {count, plural, one {# položku} few {# položky} other {# položek}} na {name, select, null {existující osobu} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {Přeřazena # položka} few {Přeřazeny # položky} other {Přeřazeno # položek}} na novou osobu", @@ -1577,6 +1619,7 @@ "regenerating_thumbnails": "Regenerace miniatur", "remote": "Vzdálený", "remote_assets": "Vzdálené položky", + "remote_media_summary": "Souhrn vzdálených médií", "remove": "Odstranit", "remove_assets_album_confirmation": "Opravdu chcete z alba odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?", "remove_assets_shared_link_confirmation": "Opravdu chcete ze sdíleného odkazu odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?", @@ -1629,6 +1672,7 @@ "restore_user": "Obnovit uživatele", "restored_asset": "Položka obnovena", "resume": "Pokračovat", + "resume_paused_jobs": "Pokračovat {count, plural, one {v # pozastavené úloze} few {ve # pozastavených úlohách} other {v # pozastavených úlohách}}", "retry_upload": "Opakování nahrávání", "review_duplicates": "Kontrola duplicit", "review_large_files": "Kontrola velkých souborů", @@ -1641,7 +1685,7 @@ "saved_api_key": "API klíč uložen", "saved_profile": "Profil uložen", "saved_settings": "Nastavení uloženo", - "say_something": "Řekněte něco", + "say_something": "Napište něco", "scaffold_body_error_occurred": "Došlo k chybě", "scan_all_libraries": "Prohledat všechny knihovny", "scan_library": "Prohledat", @@ -1722,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album", "selected": "Vybráno", "selected_count": "{count, plural, one {# vybraný} few {# vybrané} other {# vybraných}}", + "selected_gps_coordinates": "Vybrané GPS souřadnice", "send_message": "Odeslat zprávu", "send_welcome_email": "Poslat uvítací e-mail", "server_endpoint": "Koncový bod serveru", @@ -1850,6 +1895,7 @@ "show_slideshow_transition": "Zobrazit přechod prezentace", "show_supporter_badge": "Odznak podporovatele", "show_supporter_badge_description": "Zobrazit odznak podporovatele", + "show_text_search_menu": "Zobrazit nabídku pro vyhledávání textu", "shuffle": "Náhodný výběr", "sidebar": "Postranní panel", "sidebar_display_description": "Zobrazení odkazu na zobrazení v postranním panelu", @@ -1880,6 +1926,7 @@ "stacktrace": "Výpis zásobníku", "start": "Start", "start_date": "Počáteční datum", + "start_date_before_end_date": "Počáteční datum se musí nacházet před konečným datem", "state": "Stát", "status": "Stav", "stop_casting": "Zastavit odesílání", @@ -1904,6 +1951,8 @@ "sync_albums_manual_subtitle": "Synchronizovat všechna nahraná videa a fotografie do vybraných záložních alb", "sync_local": "Synchronizovat místní", "sync_remote": "Synchronizovat vzdálené", + "sync_status": "Stav synchronizace", + "sync_status_subtitle": "Zobrazit a spravovat synchronizační systém", "sync_upload_album_setting_subtitle": "Vytvořit a nahrát fotografie a videa do vybraných alb na Immich", "tag": "Značka", "tag_assets": "Přiřadit značku", @@ -1941,7 +1990,9 @@ "to_change_password": "Změnit heslo", "to_favorite": "Oblíbit", "to_login": "Přihlásit", + "to_multi_select": "na vícenásobný výběr", "to_parent": "Přejít k rodiči", + "to_select": "vybrat", "to_trash": "Vyhodit", "toggle_settings": "Přepnout nastavení", "total": "Celkem", @@ -1961,6 +2012,7 @@ "trash_page_select_assets_btn": "Vybrat položky", "trash_page_title": "Koš ({count})", "trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.", + "troubleshoot": "Diagnostika", "type": "Typ", "unable_to_change_pin_code": "Nelze změnit PIN kód", "unable_to_setup_pin_code": "Nelze nastavit PIN kód", @@ -1991,6 +2043,7 @@ "unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položek}}", "untagged": "Neoznačeno", "up_next": "To je prozatím vše", + "update_location_action_prompt": "Aktualizovat polohu {count} vybraných položek pomocí:", "updated_at": "Aktualizováno", "updated_password": "Heslo aktualizováno", "upload": "Nahrát", @@ -2057,6 +2110,7 @@ "view_next_asset": "Zobrazit další položku", "view_previous_asset": "Zobrazit předchozí položku", "view_qr_code": "Zobrazit QR kód", + "view_similar_photos": "Zobrazit podobné fotky", "view_stack": "Zobrazit seskupení", "view_user": "Zobrazit uživatele", "viewer_remove_from_stack": "Odstranit ze zásobníku", @@ -2075,5 +2129,6 @@ "yes": "Ano", "you_dont_have_any_shared_links": "Nemáte žádné sdílené odkazy", "your_wifi_name": "Název vaší Wi-Fi", - "zoom_image": "Zvětšit obrázek" + "zoom_image": "Zvětšit obrázek", + "zoom_to_bounds": "Přiblížit na okraje" } diff --git a/i18n/da.json b/i18n/da.json index d42bc475ab..61891e7ef8 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -1,8 +1,8 @@ { - "about": "Om", + "about": "Om os", "account": "Konto", "account_settings": "Kontoindstillinger", - "acknowledge": "Godkend", + "acknowledge": "Accepter", "action": "Handling", "action_common_update": "Opdater", "actions": "Handlinger", @@ -28,6 +28,9 @@ "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_album_toggle": "Skift selektion for {album}", + "add_to_albums": "Tilføj til albummer", + "add_to_albums_count": "Tilføj til albummer({count})", "add_to_shared_album": "Tilføj til delt album", "add_url": "Tilføj URL", "added_to_archive": "Tilføjet til arkiv", @@ -51,6 +54,7 @@ "backup_onboarding_description": "En 3-2-1 backup strategy anbefales for at beskytte dine data. En altomfattende backupløsning skulle gerne have kopier af dine uploadede billeder og videoer, samt Immich databasen.", "backup_onboarding_footer": "Referer venligst til dokumentationen for mere information om hvordan Immich backes op.", "backup_onboarding_parts_title": "En 3-2-1 backup inkluderer:", + "backup_onboarding_title": "Backupper", "backup_settings": "Database Backup-indstillinger", "backup_settings_description": "Administrer backupindstillinger for database.", "cleared_jobs": "Ryddet jobs til: {job}", @@ -119,16 +123,23 @@ "logging_enable_description": "Aktiver logning", "logging_level_description": "Når slået til, hvilket logniveau, der skal bruges.", "logging_settings": "Logning", + "machine_learning_availability_checks": "Tilgængelighedstjek", + "machine_learning_availability_checks_description": "Opdag og foretræk automatisk tilgængelige maskinlæringsservere", + "machine_learning_availability_checks_enabled": "Aktivér tilgængelighedstjek", + "machine_learning_availability_checks_interval": "Kontroller interval", + "machine_learning_availability_checks_interval_description": "Interval i millisekunder mellem tilgængelighedstjeks", + "machine_learning_availability_checks_timeout": "Timeout på anmodning", + "machine_learning_availability_checks_timeout_description": "Timeout i millisekunder på tilgængelighedstjeks", "machine_learning_clip_model": "CLIP-model", "machine_learning_clip_model_description": "Navnet på CLIP-modellen på listen her. Bemærk at du skal genkøre \"Smart Søgning\"-jobbet for alle billeder, hvis du skifter model.", "machine_learning_duplicate_detection": "Dubletdetektion", - "machine_learning_duplicate_detection_enabled": "Aktiver duplikatdetektion", - "machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler blive de-duplikerede.", - "machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige duplikater", + "machine_learning_duplicate_detection_enabled": "Aktiver dubletdetektion", + "machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler stadig blive de-duplikerede.", + "machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige dubletter", "machine_learning_enabled": "Aktivér maskinlæring", "machine_learning_enabled_description": "Hvis deaktiveret, vil alle ML-funktioner blive deaktiveret uanset nedenstående indstillinger.", "machine_learning_facial_recognition": "Ansigtsgenkendelse", - "machine_learning_facial_recognition_description": "Registrer, genkend og grupper ansigter i billeder", + "machine_learning_facial_recognition_description": "Opdag, genkend og gruppér ansigter i billeder", "machine_learning_facial_recognition_model": "Ansigtsgenkendelsesmodel", "machine_learning_facial_recognition_model_description": "Modellerne er listet i faldende størrelsesorden. Større modeller er langsommere og bruger mere hukommelse, men giver bedre resultater. Bemærk, at du skal køre ansigtsopdagelsesopgaven igen for alle billeder, når du ændrer en model.", "machine_learning_facial_recognition_setting": "Aktivér ansigtgenkendelse", @@ -217,6 +228,8 @@ "oauth_mobile_redirect_uri": "Mobilomdiregerings-URL", "oauth_mobile_redirect_uri_override": "Tilsidesættelse af mobil omdiregerings-URL", "oauth_mobile_redirect_uri_override_description": "Aktiver, når OAuth-udbyderen ikke tillader en mobil URI, som ''{callback}''", + "oauth_role_claim": "Rolle attribut", + "oauth_role_claim_description": "Tildel automatisk admin adgang på basis af forekomst af denne påstand. Dén kan være enten 'user' eller 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Administrer OAuth login-indstillinger", "oauth_settings_more_details": "Læs flere detaljer om funktionen i dokumentationen.", @@ -265,6 +278,7 @@ "storage_template_migration_info": "Lager-skabelonen vil konvertere alle filendelser til små bogstaver. Skabelonændringer vil kun gælde for nye mediefiler. For at anvende skabelonen retroaktivt på tidligere uploadede mediefiler skal du køre {job}.", "storage_template_migration_job": "Lager Skabelon Migreringsjob", "storage_template_more_details": "For flere detaljer om denne funktion, referer til Lager Skabelonen og dens implikationer", + "storage_template_onboarding_description_v2": "Når aktiveret, så vil denne funktion auto-organisere filer på grundlag af en brugerdefineret skabelon. For nærmere, se dokumentation.", "storage_template_path_length": "Anslået sti-længde begrænsning {length, number}/{limit, number}", "storage_template_settings": "Lagringsskabelon", "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil", @@ -351,7 +365,9 @@ "trash_number_of_days_description": "Antal dage aktiver i skraldespanden skal beholdes inden de fjernes permanent", "trash_settings": "Skraldeindstillinger", "trash_settings_description": "Administrér skraldeindstillinger", + "unlink_all_oauth_accounts": "Ophæv link til alle OAuth konti", "unlink_all_oauth_accounts_description": "Husk at fjerne linket til alle OAuth konti før du migrerer til en ny udbyder.", + "unlink_all_oauth_accounts_prompt": "Er du sikker på, at du vil ophæve link til alle OAuth konti? Dette vil nulstille OAuth ID for hver bruger og kan ikke fortrydes.", "user_cleanup_job": "Bruger-oprydning", "user_delete_delay": "{user}'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.", "user_delete_delay_settings": "Slet forsinkelse", @@ -378,15 +394,15 @@ "admin_password": "Administratoradgangskode", "administration": "Administration", "advanced": "Avanceret", - "advanced_settings_beta_timeline_subtitle": "Prøv den nye app-oplevelse", - "advanced_settings_beta_timeline_title": "Beta-tidslinje", "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_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 er meget lang tid om at indlæse miniaturebilleder af lokale elementer. Aktiver denne indstilling for at indlæse elementer fra serveren i stedet.", "advanced_settings_prefer_remote_title": "Foretræk elementer på serveren", "advanced_settings_proxy_headers_subtitle": "Definer proxy headers Immich skal sende med hver netværks forespørgsel", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_title": "Proxy headere", + "advanced_settings_readonly_mode_subtitle": "Aktiverer skrivebeskyttet tilstand, hvor billederne alene kan vises. Ting som at vælge flere billeder, dele, caste og slette er alle deaktiveret. Aktiver skrivebeskyttet tilstand via en bruger avatar fra hovedskærmen", + "advanced_settings_readonly_mode_title": "Skrivebeskyttet tilstand", "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", @@ -402,6 +418,7 @@ "album_cover_updated": "Albumcover opdateret", "album_delete_confirmation": "Er du sikker på at du vil slette albummet {album}?", "album_delete_confirmation_description": "Hvis dette album er delt, vil andre brugere ikke længere kunne få adgang til det.", + "album_deleted": "Album slettet", "album_info_card_backup_album_excluded": "EKSKLUDERET", "album_info_card_backup_album_included": "INKLUDERET", "album_info_updated": "Albuminfo opdateret", @@ -411,7 +428,9 @@ "album_options": "Albumindstillinger", "album_remove_user": "Fjern bruger?", "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?", + "album_search_not_found": "Ingen album fundet som matcher din søgning", "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_summary": "Albumoversigt", "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}", @@ -430,6 +449,7 @@ "albums_default_sort_order": "Standard album sortering", "albums_default_sort_order_description": "Grundlæggende sortering ved oprettelse af nyt album.", "albums_feature_description": "Samling af billeder der kan deles med andre brugere.", + "albums_on_device_count": "Albummer på enheden ({count})", "all": "Alt", "all_albums": "Alle albummer", "all_people": "Alle personer", @@ -449,7 +469,9 @@ "app_bar_signout_dialog_title": "Log ud", "app_settings": "Appindstillinger", "appears_in": "Optræder i", + "apply_count": "Brug ({count, number})", "archive": "Arkiv", + "archive_action_prompt": "{count} føjet til 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 ({count})", @@ -480,14 +502,18 @@ "asset_restored_successfully": "Elementet blev gendannet succesfuldt", "asset_skipped": "Sprunget over", "asset_skipped_in_trash": "I skraldespand", + "asset_trashed": "Objekt kasseret", + "asset_troubleshoot": "Fejlsøg på objekt", "asset_uploaded": "Uploadet", "asset_uploading": "Uploader…", "asset_viewer_settings_subtitle": "Administrer indstillinger for gallerifremviser", "asset_viewer_settings_title": "Billedviser", - "assets": "elementer", + "assets": "Objekter", "assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}", "assets_added_to_album_count": "{count, plural, one {# mediefil} other {# mediefiler}} tilføjet til albummet", + "assets_added_to_albums_count": "Tilføjet {assetTotal, plural, one {# asset} other {# assets}} til {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Billed} other {Billeder}} kan ikke blive tilføjet til album", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} kan ikke føjes til i nogen af albummerne", "assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}", "assets_deleted_permanently": "{count} element(er) blev fjernet permanent", "assets_deleted_permanently_from_server": "{count} element(er) blev fjernet permanent fra Immich serveren", @@ -504,14 +530,17 @@ "assets_trashed_count": "{count, plural, one {# mediefil} other {# mediefiler}} smidt i papirkurven", "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", + "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} er allerede en del af albummerne", "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", "automatic_endpoint_switching_title": "Automatisk skift af URL", "autoplay_slideshow": "Afspil slideshow automatisk", "back": "Tilbage", "back_close_deselect": "Tilbage, luk eller fravælg", + "background_backup_running_error": "Backup kører lige nu i baggrund; kan ikke starte manuel backup", "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", + "background_options": "Baggrundsmuligheder", "backup": "Sikkerhedskopier", "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", @@ -519,6 +548,7 @@ "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_albums_sync": "Synkronisering af backupalbums", "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…", @@ -568,13 +598,15 @@ "backup_controller_page_turn_on": "Slå sikkerhedskopiering til", "backup_controller_page_uploading_file_info": "Uploader filinformation", "backup_err_only_album": "Kan ikke slette det eneste album", - "backup_info_card_assets": "elementer", + "backup_info_card_assets": "objekter", "backup_manual_cancelled": "Annulleret", "backup_manual_in_progress": "Upload er allerede undervejs. Prøv igen efter noget tid", "backup_manual_success": "Succes", "backup_manual_title": "Uploadstatus", + "backup_options": "Backup indstillinger", "backup_options_page_title": "Backupindstillinger", "backup_setting_subtitle": "Administrer indstillnger for upload i forgrund og baggrund", + "backup_settings_subtitle": "Håndtere upload indstillinger", "backward": "Baglæns", "biometric_auth_enabled": "Biometrisk adgangskontrol slået til", "biometric_locked_out": "Du er låst ude af biometrisk adgangskontrol", @@ -610,10 +642,11 @@ "cancel": "Annullér", "cancel_search": "Annullér søgning", "canceled": "Annulleret", + "canceling": "Annullerer", "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", - "cast": "Cast", + "cast": "Caste", "cast_description": "Konfigurer tilgængelige cast destinationer", "change_date": "Ændr dato", "change_description": "Beskrivelse af ændringer", @@ -632,6 +665,8 @@ "change_pin_code": "Skift PIN kode", "change_your_password": "Skift dit kodeord", "changed_visibility_successfully": "Synlighed blev ændret", + "charging": "Lader", + "charging_requirement_mobile_backup": "Baggrundsbackup kræver, at enheden er tilsluttet oplader", "check_corrupt_asset_backup": "Tjek for korrupte sikkerhedskopier af elementer", "check_corrupt_asset_backup_button": "Foretag kontrol", "check_corrupt_asset_backup_description": "Kør kun denne kontrol via Wi-Fi, og når alle elementer er blevet sikkerhedskopieret. Proceduren kan tage et par minutter.", @@ -641,6 +676,7 @@ "clear": "Ryd", "clear_all": "Ryd alle", "clear_all_recent_searches": "Ryd alle seneste søgninger", + "clear_file_cache": "Ryd filcache", "clear_message": "Ryd bedsked", "clear_value": "Ryd værdi", "client_cert_dialog_msg_confirm": "OK", @@ -711,11 +747,13 @@ "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_link": "Opret delt link", "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", + "creating_linked_albums": "Opretter sammenkædede albums...", "crop": "Beskær", "curated_object_page_title": "Ting", "current_device": "Nuværende enhed", @@ -723,9 +761,11 @@ "current_server_address": "Nuværende serveraddresse", "custom_locale": "Brugerdefineret lokale", "custom_locale_description": "Formatér datoer og tal baseret på sproget og regionen", + "custom_url": "Tilpasset URL", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Mørk", + "dark_theme": "Skift til mørkt tema", "date_after": "Dato efter", "date_and_time": "Dato og klokkeslæt", "date_before": "Dato før", @@ -733,6 +773,7 @@ "date_of_birth_saved": "Fødselsdatoen blev gemt korrekt", "date_range": "Datointerval", "day": "Dag", + "days": "Dage", "deduplicate_all": "Kopier alle", "deduplication_criteria_1": "Billedstørrelse i bytes", "deduplication_criteria_2": "Antal EXIF-data", @@ -741,6 +782,8 @@ "default_locale": "Standardlokalitet", "default_locale_description": "Formatér datoer og tal baseret på din browsers regions indstillinger", "delete": "Slet", + "delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt", + "delete_action_prompt": "{count} slettet", "delete_album": "Slet album", "delete_api_key_prompt": "Er du sikker på, at du vil slette denne API-nøgle?", "delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed", @@ -754,9 +797,12 @@ "delete_key": "Slet nøgle", "delete_library": "Slet bibliotek", "delete_link": "Slet link", + "delete_local_action_prompt": "{count} slettet lokalt", "delete_local_dialog_ok_backed_up_only": "Slet kun backup", "delete_local_dialog_ok_force": "Slet alligevel", "delete_others": "Slet andre", + "delete_permanently": "Slet permanent", + "delete_permanently_action_prompt": "{count} slettet permanent", "delete_shared_link": "Slet delt link", "delete_shared_link_dialog_title": "Slet delt link", "delete_tag": "Slet tag", @@ -767,6 +813,7 @@ "description": "Beskrivelse", "description_input_hint_text": "Tilføj en beskrivelse...", "description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer", + "deselect_all": "Afmarkér alt", "details": "DETALJER", "direction": "Retning", "disabled": "Deaktiveret", @@ -784,6 +831,7 @@ "documentation": "Dokumentation", "done": "Færdig", "download": "Hent", + "download_action_prompt": "Downloader {count} objekter", "download_canceled": "Download annulleret", "download_complete": "Download fuldført", "download_enqueue": "Donload sat i kø", @@ -810,8 +858,12 @@ "edit": "Rediger", "edit_album": "Redigér album", "edit_avatar": "Redigér avatar", + "edit_birthday": "Rediger fødselsdag", "edit_date": "Redigér dato", "edit_date_and_time": "Redigér dato og tid", + "edit_date_and_time_action_prompt": "{count} dato og tid redigeret", + "edit_date_and_time_by_offset": "Forskyde dato med offset", + "edit_date_and_time_by_offset_interval": "Nyt datointerval: {from} - {to}", "edit_description": "Rediger beskrivelse", "edit_description_prompt": "Vælg venligst en ny beskrivelse:", "edit_exclusion_pattern": "Redigér udelukkelsesmønster", @@ -821,6 +873,7 @@ "edit_key": "Redigér nøgle", "edit_link": "Rediger link", "edit_location": "Rediger placering", + "edit_location_action_prompt": "{count} geolokation redigeret", "edit_location_dialog_title": "Placering", "edit_name": "Rediger navn", "edit_people": "Redigér personer", @@ -839,6 +892,7 @@ "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_backup": "Aktiver backup", "enable_biometric_auth_description": "Indtast din PIN kode for at slå biometrisk adgangskontrol til", "enabled": "Aktiveret", "end_date": "Slutdato", @@ -849,7 +903,9 @@ "error": "Fejl", "error_change_sort_album": "Ændring af sorteringsrækkefølgen mislykkedes", "error_delete_face": "Fejl ved sletning af ansigt fra mediefil", + "error_getting_places": "Fejl ved hentning af steder", "error_loading_image": "Fejl ved indlæsning af billede", + "error_loading_partners": "Fejl ved indlæsning af partnere: {error}", "error_saving_image": "Fejl: {error}", "error_tag_face_bounding_box": "Fejl ved tagging af ansigt - kan ikke finde koordinator for afgrænsningskasse", "error_title": "Fejl - Noget gik galt", @@ -882,6 +938,7 @@ "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_reset_pin_code": "Kunne ikke resette PIN-koden", "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", @@ -890,6 +947,7 @@ "paths_validation_failed": "{paths, plural, one {# sti} other {# stier}} slog fejl ved validering", "profile_picture_transparent_pixels": "Profilbilleder kan ikke have gennemsigtige pixels. Zoom venligst ind og/eller flyt billedet.", "quota_higher_than_disk_size": "Du har sat en kvote der er større end disken", + "something_went_wrong": "Noget gik galt", "unable_to_add_album_users": "Ikke i stand til at tilføje brugere til album", "unable_to_add_assets_to_shared_link": "Kan ikke tilføje mediefiler til det delte link", "unable_to_add_comment": "Ikke i stand til at tilføje kommentar", @@ -975,6 +1033,7 @@ }, "exif": "Exif", "exif_bottom_sheet_description": "Tilføj beskrivelse...", + "exif_bottom_sheet_description_error": "Fejl ved opdatering af beskrivelsen", "exif_bottom_sheet_details": "DETALJER", "exif_bottom_sheet_location": "LOKATION", "exif_bottom_sheet_people": "PERSONER", @@ -992,6 +1051,8 @@ "explorer": "Udforske", "export": "Eksportér", "export_as_json": "Eksportér som JSON", + "export_database": "Eksporter database", + "export_database_description": "Eksporter SQLite databasen", "extension": "Udvidelse", "external": "Ekstern", "external_libraries": "Eksterne biblioteker", @@ -1003,11 +1064,13 @@ "failed_to_load_assets": "Kunne ikke indlæse mediefiler", "failed_to_load_folder": "Kunne ikke indlæse mappe", "favorite": "Favorit", + "favorite_action_prompt": "{count} føjet til favoritter", "favorite_or_unfavorite_photo": "Tilføj eller fjern fra yndlingsbilleder", "favorites": "Favoritter", "favorites_page_no_favorites": "Ingen favoritter blev fundet", "feature_photo_updated": "Forsidebillede uploadet", "features": "Funktioner", + "features_in_development": "Funktioner under udvikling", "features_setting_description": "Administrer app-funktioner", "file_name": "Filnavn", "file_name_or_extension": "Filnavn eller filtype", @@ -1017,21 +1080,26 @@ "filter_people": "Filtrér personer", "filter_places": "Filtrer steder", "find_them_fast": "Find dem hurtigt med søgning via navn", + "first": "Første", "fix_incorrect_match": "Fix forkert match", "folder": "Mappe", "folder_not_found": "Mappe ikke fundet", "folders": "Mapper", "folders_feature_description": "Gennemse mappevisningen efter fotos og videoer på filsystemet", + "forgot_pin_code_question": "Har du glemt PIN-koden?", "forward": "Fremad", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funktion indlæser eksterne ressourcer fra Google for at virke.", "general": "Generel", + "geolocation_instruction_location": "Klik på et objekt med GPS-koordinater for at bruge dettes position, eller vælg position direkte på kortet", "get_help": "Få hjælp", "get_wifiname_error": "Kunne ikke hente Wi-Fi-navn. Sørg for, at du har givet de nødvendige tilladelser og er forbundet til et Wi-Fi-netværk", "getting_started": "Kom godt i gang", "go_back": "Gå tilbage", "go_to_folder": "Gå til mappe", "go_to_search": "Gå til søgning", + "gps": "GPS", + "gps_missing": "Ingen GPS", "grant_permission": "Giv tilladelse", "group_albums_by": "Gruppér albummer efter...", "group_country": "Gruppér efter land", @@ -1042,6 +1110,9 @@ "haptic_feedback_switch": "Slå haptisk feedback til", "haptic_feedback_title": "Haptisk feedback", "has_quota": "Har kvote", + "hash_asset": "Hash objekter", + "hashed_assets": "Hashede objekter", + "hashing": "Hasher", "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", @@ -1073,7 +1144,9 @@ "home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over", "host": "Host", "hour": "Time", + "hours": "Timer", "id": "ID", + "idle": "Inaktiv", "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", @@ -1131,10 +1204,13 @@ "language_no_results_title": "Ingen sprog fundet", "language_search_hint": "Vælg sprog...", "language_setting_description": "Vælg dit foretrukne sprog", + "large_files": "Store filer", + "last": "Sidste", "last_seen": "Sidst set", "latest_version": "Seneste version", "latitude": "Breddegrad", "leave": "Forlad", + "leave_album": "Forlad album", "lens_model": "Objektivmodel", "let_others_respond": "Lad andre svare", "level": "Niveau", @@ -1146,7 +1222,9 @@ "library_page_sort_created": "Senest oprettet", "library_page_sort_last_modified": "Sidst redigeret", "library_page_sort_title": "Albumtitel", + "licenses": "Licenser", "light": "Lys", + "like": "Synes om", "like_deleted": "Ligesom slettet", "link_motion_video": "Link bevægelsesvideo", "link_to_oauth": "Link til OAuth", @@ -1154,7 +1232,10 @@ "list": "Liste", "loading": "Indlæser", "loading_search_results_failed": "Indlæsning af søgeresultater fejlede", + "local": "Lokal", "local_asset_cast_failed": "Kan ikke caste et aktiv, der ikke er uploadet til serveren", + "local_assets": "Lokale objekter", + "local_media_summary": "Opsummering af lokale media", "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", @@ -1166,8 +1247,10 @@ "location_picker_longitude_hint": "Indtast din længdegrad her", "lock": "Lås", "locked_folder": "Låst mappe", + "log_detail_title": "Logdetaljer", "log_out": "Log ud", "log_out_all_devices": "Log ud af alle enheder", + "logged_in_as": "Logget ind som {user}", "logged_out_all_devices": "Logget ud af alle enheder", "logged_out_device": "Logget ud af enhed", "login": "Log ind", @@ -1176,7 +1259,7 @@ "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_endpoint_url": "Server endepunkt 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", @@ -1195,6 +1278,7 @@ "login_password_changed_success": "Kodeordet blev opdateret", "logout_all_device_confirmation": "Er du sikker på, at du vil logge ud af alle enheder?", "logout_this_device_confirmation": "Er du sikker på, at du vil logge denne enhed ud?", + "logs": "Logs", "longitude": "Længdegrad", "look": "Kig", "loop_videos": "Gentag videoer", @@ -1202,6 +1286,7 @@ "main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!", "main_menu": "Hovedmenu", "make": "Producent", + "manage_geolocation": "Administrer placering", "manage_shared_links": "Håndter delte links", "manage_sharing_with_partners": "Administrér deling med partnere", "manage_the_app_settings": "Administrer appindstillinger", @@ -1236,6 +1321,7 @@ "mark_as_read": "Marker som læst", "marked_all_as_read": "Markerede alle som læst", "matches": "Parringer", + "matching_assets": "Matchende objekter", "media_type": "Medietype", "memories": "Minder", "memories_all_caught_up": "Ajour", @@ -1254,6 +1340,7 @@ "merged_people_count": "{count, plural, one {# person} other {# personer}} lagt sammen", "minimize": "Minimér", "minute": "Minut", + "minutes": "Minutter", "missing": "Mangler", "model": "Model", "month": "Måned", @@ -1261,6 +1348,7 @@ "more": "Mere", "move": "Flyt", "move_off_locked_folder": "Flyt ud af låst mappe", + "move_to_lock_folder_action_prompt": "{count} føjet til i den låste 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", @@ -1272,6 +1360,10 @@ "my_albums": "Mine albummer", "name": "Navn", "name_or_nickname": "Navn eller kælenavn", + "network_requirement_photos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine fotos", + "network_requirement_videos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine videoer", + "network_requirements": "Netværkskrav", + "network_requirements_updated": "Netværkskravene er ændret, backup-køen nulstilles", "networking_settings": "Netværk", "networking_subtitle": "Administrer serverens endepunktindstillinger", "never": "aldrig", @@ -1281,6 +1373,7 @@ "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_timeline": "Ny tidslinje", "new_user_created": "Ny bruger oprettet", "new_version_available": "NY VERSION TILGÆNGELIG", "newest_first": "Nyeste først", @@ -1294,19 +1387,25 @@ "no_assets_message": "KLIK FOR AT UPLOADE DIT FØRSTE BILLEDE", "no_assets_to_show": "Ingen elementer at vise", "no_cast_devices_found": "Ingen Cast-enheder fundet", + "no_checksum_local": "Ingen checksum tilgængelig – kan ikke hente lokale objekter", + "no_checksum_remote": "Ingen checksum tilgængelig – kan ikke hente eksterne objekter", "no_duplicates_found": "Ingen duplikater fundet.", "no_exif_info_available": "Ingen tilgængelig exif information", "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_local_assets_found": "Ingen lokale objekter fundet med denne checksum", "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_remote_assets_found": "Ingen eksterne objekter fundet med denne checksum", "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", + "no_uploads_in_progress": "Ingen upload i gang", + "not_available": "ikke tilgængelig", "not_in_any_album": "Ikke i noget album", "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", @@ -1322,6 +1421,7 @@ "oauth": "OAuth", "official_immich_resources": "Officielle Immich-ressourcer", "offline": "Offline", + "offset": "Forskydning", "ok": "Ok", "oldest_first": "Ældste først", "on_this_device": "På denne enhed", @@ -1340,14 +1440,17 @@ "open_the_search_filters": "Åbn søgefiltre", "options": "Handlinger", "or": "eller", + "organize_into_albums": "Organiser i album", + "organize_into_albums_description": "Sæt eksisterende billeder i albummer ved hjælp af aktuelle synkroniseringsindstillinger", "organize_your_library": "Organisér dit bibliotek", "original": "original", "other": "Andet", "other_devices": "Andre enheder", + "other_entities": "Andre enheder", "other_variables": "Andre variable", "owned": "Egne", "owner": "Ejer", - "partner": "Partner", + "partner": "Partnerpartner", "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", @@ -1397,7 +1500,10 @@ "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": "Personperson", + "person_age_months": "{months, plural, one {# month} other {# months}} gammel", + "person_age_year_months": "1 år, {months, plural, one {# month} other {# months}} gammel", + "person_age_years": "{years, plural, other {# years}} gammel", "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.", @@ -1421,6 +1527,7 @@ "port": "Port", "preferences_settings_subtitle": "Administrer app-præferencer", "preferences_settings_title": "Præferencer", + "preparing": "Forberedelse", "preset": "Forudindstilling", "preview": "Forhåndsvisning", "previous": "Forrige", @@ -1437,6 +1544,7 @@ "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_readonly_mode": "Skrivebeskyttet tilstand aktiveret. Lang tryk på bruger avatar ikonet for at afslutte.", "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}", @@ -1472,15 +1580,20 @@ "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_description_2": "Supporterstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren", + "query_asset_id": "Forespørgsels Asset ID", + "queue_status": "Kø {count}/{total}", "rating": "Stjernebedømmelse", "rating_clear": "Nulstil vurdering", "rating_count": "{count, plural, one {# stjerne} other {# stjerner}}", "rating_description": "Vis EXIF-klassificeringen i infopanelet", "reaction_options": "Reaktionsindstillinger", "read_changelog": "Læs ændringslog", + "readonly_mode_disabled": "Skrivebeskyttet tilstand deaktiveret", + "readonly_mode_enabled": "Skrivebeskyttet tilstand aktiveret", + "ready_for_upload": "Klar til upload", "reassign": "Gentildel", "reassigned_assets_to_existing_person": "{count, plural, one {# mediefil} other {# mediefiler}} er blevet gentildelt til {name, select, null {en eksisterende person} other {{name}}}", "reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person", @@ -1503,6 +1616,9 @@ "refreshing_faces": "Opdaterer ansigter", "refreshing_metadata": "Opdaterer metadata", "regenerating_thumbnails": "Regenererer forhåndsvisninger", + "remote": "Eksternt", + "remote_assets": "Eksterne objekter", + "remote_media_summary": "Oversigt over eksterne media", "remove": "Fjern", "remove_assets_album_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra albummet?", "remove_assets_shared_link_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra dette delte link?", @@ -1510,7 +1626,9 @@ "remove_custom_date_range": "Fjern tilpasset datointerval", "remove_deleted_assets": "Fjern slettede mediefiler", "remove_from_album": "Fjern fra album", + "remove_from_album_action_prompt": "{count} fjernet fra albummet", "remove_from_favorites": "Fjern fra favoritter", + "remove_from_lock_folder_action_prompt": "{count} fjernet fra den låste mappe", "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", @@ -1538,19 +1656,29 @@ "reset_password": "Nulstil adgangskode", "reset_people_visibility": "Nulstil personsynlighed", "reset_pin_code": "Nulstil PIN kode", + "reset_pin_code_description": "Hvis du har glemt din PIN-kode, kan du kontakte serveradministratoren for at få den stillet tilbage", + "reset_pin_code_success": "PIN-koden er stillet tilbage", + "reset_pin_code_with_password": "Du kan altid nulstille din PIN-kode med dit password", + "reset_sqlite": "Reset SQLite Databasen", + "reset_sqlite_confirmation": "Er du sikker på, at du vil nulstille SQLite databasen? Du er nødt til at logge ud og ind igen for at gensynkronisere dine data", + "reset_sqlite_success": "Vellykket reset af SQLite databasen", "reset_to_default": "Nulstil til standard", "resolve_duplicates": "Løs dubletter", "resolved_all_duplicates": "Alle dubletter løst", "restore": "Gendan", "restore_all": "Gendan alle", + "restore_trash_action_prompt": "{count} genskabt fra papirkurven", "restore_user": "Gendan bruger", "restored_asset": "Gendannet mediefilen", "resume": "Genoptag", + "resume_paused_jobs": "Fortsæt {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Forsøg upload igen", "review_duplicates": "Gennemgå dubletter", + "review_large_files": "Gennemgå store filer", "role": "Rolle", "role_editor": "Redaktør", "role_viewer": "Seer", + "running": "Kører", "save": "Gem", "save_to_gallery": "Gem til galleri", "saved_api_key": "Gemt API-nøgle", @@ -1623,6 +1751,7 @@ "select_album_cover": "Vælg albumcover", "select_all": "Vælg alle", "select_all_duplicates": "Vælg alle dubletter", + "select_all_in": "Vælg alt i {group}", "select_avatar_color": "Vælg avatarfarve", "select_face": "Vælg ansigt", "select_featured_photo": "Vælg forsidebillede", @@ -1636,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album", "selected": "Valgt", "selected_count": "{count, plural, one {# valgt} other {# valgte}}", + "selected_gps_coordinates": "Udvalgte GPS Koordinater", "send_message": "Send besked", "send_welcome_email": "Send velkomstemail", "server_endpoint": "Server endepunkt", @@ -1673,7 +1803,7 @@ "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_looping_title": "Looper", "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", @@ -1681,6 +1811,7 @@ "settings_saved": "Indstillinger er gemt", "setup_pin_code": "Sæt in PIN kode", "share": "Del", + "share_action_prompt": "Delte {count} objekter", "share_add_photos": "Tilføj billeder", "share_assets_selected": "{count} valgt", "share_dialog_preparing": "Forbereder...", @@ -1702,6 +1833,7 @@ "shared_link_clipboard_copied_massage": "Kopieret til udklipsholderen", "shared_link_clipboard_text": "Link: {link}\nAdgangskode: {password}", "shared_link_create_error": "Der opstod en fejl i oprettelsen af et delt link", + "shared_link_custom_url_description": "Adgang til dette delte link med en selvdefineret URL", "shared_link_edit_description_hint": "Indtast beskrivelse", "shared_link_edit_expire_after_option_day": "1 dag", "shared_link_edit_expire_after_option_days": "{count} dage", @@ -1727,6 +1859,7 @@ "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Håndter delte links", "shared_link_options": "Muligheder for delt link", + "shared_link_password_description": "Kræv et kodeord for at få adgang til dette delte link", "shared_links": "Delte links", "shared_links_description": "Del billeder og videoer med et link", "shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}", @@ -1761,6 +1894,7 @@ "show_slideshow_transition": "Vis overgang til diasshow", "show_supporter_badge": "Supportermærke", "show_supporter_badge_description": "Vis et supportermærke", + "show_text_search_menu": "Vis tekstsøgningsmenu", "shuffle": "Bland", "sidebar": "Sidebjælke", "sidebar_display_description": "Vis et link til visningen i sidebjælken", @@ -1776,12 +1910,14 @@ "sort_created": "Dato oprettet", "sort_items": "Antal genstande", "sort_modified": "Ændret dato", + "sort_newest": "Nyeste foto", "sort_oldest": "Ældste foto", "sort_people_by_similarity": "Sorter efter personer der ligner hinanden", "sort_recent": "Seneste foto", "sort_title": "Titel", "source": "Kilde", "stack": "Stak", + "stack_action_prompt": "{count} stakket", "stack_duplicates": "Stak dubletter", "stack_select_one_photo": "Vælg ét hovedbillede til stakken", "stack_selected_photos": "Stak valgte billeder", @@ -1789,9 +1925,10 @@ "stacktrace": "Stacktrace", "start": "Start", "start_date": "Startdato", + "start_date_before_end_date": "Startdato skal ligge før slutdato", "state": "Stat", "status": "Status", - "stop_casting": "Stop casting", + "stop_casting": "Stop støbning", "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.", @@ -1801,6 +1938,7 @@ "storage_quota": "Lagringskvota", "storage_usage": "{used} ud af {available} brugt", "submit": "Indsend", + "success": "Vellykket", "suggestions": "Anbefalinger", "sunrise_on_the_beach": "Solopgang på stranden", "support": "Support", @@ -1810,6 +1948,10 @@ "sync": "Synkronisér", "sync_albums": "Synkroniser albummer", "sync_albums_manual_subtitle": "Synkroniser alle uploadet billeder og videoer til de valgte backupalbummer", + "sync_local": "Synkroniser lokalt", + "sync_remote": "Synkroniser eksternt", + "sync_status": "Synkroniserings Status", + "sync_status_subtitle": "Se og administrér synkroniseringssystemet", "sync_upload_album_setting_subtitle": "Opret og upload dine billeder og videoer til de valgte albummer i Immich", "tag": "Tag", "tag_assets": "Tag mediefiler", @@ -1820,6 +1962,7 @@ "tag_updated": "Opdateret tag: {tag}", "tagged_assets": "Tagget {count, plural, one {# aktiv} other {# aktiver}}", "tags": "Tags", + "tap_to_run_job": "Tryk for at køre jobbet", "template": "Skabelon", "theme": "Tema", "theme_selection": "Temavalg", @@ -1846,12 +1989,15 @@ "to_change_password": "Skift adgangskode", "to_favorite": "Gør til favorit", "to_login": "Login", + "to_multi_select": "For at vælge flere", "to_parent": "Gå op", + "to_select": "for at vælge", "to_trash": "Papirkurv", "toggle_settings": "Slå indstillinger til eller fra", "total": "Total", "total_usage": "Samlet forbrug", "trash": "Papirkurv", + "trash_action_prompt": "{count} flyttet til papirkurven", "trash_all": "Smid alle ud", "trash_count": "Slet {count, number}", "trash_delete_asset": "Flyt mediefil til Papirkurv", @@ -1865,13 +2011,16 @@ "trash_page_select_assets_btn": "Vælg elementer", "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}}.", + "troubleshoot": "Fejlfinding", "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", + "unarchive_action_prompt": "{count} slettet fra Arkiv", "unarchived_count": "{count, plural, other {Uarkiveret #}}", "undo": "Fortryd", "unfavorite": "Fjern favorit", + "unfavorite_action_prompt": "{count} slettet fra Favoritter", "unhide_person": "Stop med at skjule person", "unknown": "Ukendt", "unknown_country": "Ukendt land", @@ -1887,16 +2036,23 @@ "unsaved_change": "Ændring, der ikke er gemt", "unselect_all": "Fravælg alle", "unselect_all_duplicates": "Fjern markeringen af alle dubletter", + "unselect_all_in": "Afmarkér alle i {group}", "unstack": "Fjern fra stak", + "unstack_action_prompt": "{count} ustakket", "unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}", + "untagged": "Umærket", "up_next": "Næste", + "update_location_action_prompt": "Opdater lokationen for {count} valgte objekter med:", "updated_at": "Opdateret", "updated_password": "Opdaterede adgangskode", "upload": "Upload", + "upload_action_prompt": "{count} i kø til upload", "upload_concurrency": "Upload samtidighed", + "upload_details": "Upload detaljer", "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?", "upload_dialog_title": "Upload element", "upload_errors": "Upload afsluttet med {count, plural, one {# fejl} other {# fejl}}. Opdater siden for at se nye uploadaktiver.", + "upload_finished": "Upload fuldført", "upload_progress": "Resterende {remaining, number} - Behandlet {processed, number}/{total, number}", "upload_skipped_duplicates": "Sprang over {count, plural, one {# duplet aktiv} other {# duplikerede aktiver}}", "upload_status_duplicates": "Dubletter", @@ -1905,6 +2061,7 @@ "upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.", "upload_to_immich": "Upload til Immich ({count})", "uploading": "Uploader", + "uploading_media": "Uploader media", "url": "URL", "usage": "Forbrug", "use_biometric": "Brug biometrisk", @@ -1925,6 +2082,7 @@ "user_usage_stats_description": "Vis konto anvendelsesstatistik", "username": "Brugernavn", "users": "Brugere", + "users_added_to_album_count": "Føjet {count, plural, one {# bruker} other {# brukere}} til albummet", "utilities": "Værktøjer", "validate": "Validér", "validate_endpoint_error": "Indtast en gyldig URL", @@ -1943,6 +2101,7 @@ "view_album": "Se album", "view_all": "Se alle", "view_all_users": "Se alle brugere", + "view_details": "Vis detaljer", "view_in_timeline": "Se på tidslinjen", "view_link": "Vis Link", "view_links": "Vis links", @@ -1950,6 +2109,7 @@ "view_next_asset": "Se næste medie", "view_previous_asset": "Se forrige medie", "view_qr_code": "Vis QR kode", + "view_similar_photos": "Se lignende billeder", "view_stack": "Vis stak", "view_user": "Vis bruger", "viewer_remove_from_stack": "Fjern fra stak", @@ -1968,5 +2128,6 @@ "yes": "Ja", "you_dont_have_any_shared_links": "Du har ikke nogen delte links", "your_wifi_name": "Dit Wi-Fi navn", - "zoom_image": "Zoom billede" + "zoom_image": "Zoom billede", + "zoom_to_bounds": "Zoom til grænserne" } diff --git a/i18n/de.json b/i18n/de.json index 61304b96e4..4af1675ffc 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -18,7 +18,7 @@ "add_endpoint": "Endpunkt hinzufügen", "add_exclusion_pattern": "Ausschlussmuster hinzufügen", "add_import_path": "Importpfad hinzufügen", - "add_location": "Ort hinzufügen", + "add_location": "Standort hinzufügen", "add_more_users": "Weitere Nutzer hinzufügen", "add_partner": "Partner hinzufügen", "add_path": "Pfad hinzufügen", @@ -28,6 +28,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_album_bottom_sheet_some_local_assets": "Einige lokale Dateien konnten nicht zum Album hinzugefügt werden", "add_to_album_toggle": "Auswahl umschalten für {album}", "add_to_albums": "Zu Alben hinzufügen", "add_to_albums_count": "Zu Alben hinzufügen ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Aktiviere Logging", "logging_level_description": "Wenn aktiviert, welches Log-Level genutzt wird.", "logging_settings": "Protokollierung", + "machine_learning_availability_checks": "Verfügbarkeitschecks", + "machine_learning_availability_checks_description": "Erkenne und bevorzuge verfügbare Machine Learning Servers", + "machine_learning_availability_checks_enabled": "Verfügbarkeitschecks einschalten", + "machine_learning_availability_checks_interval": "Überprüfungsinterval", + "machine_learning_availability_checks_interval_description": "Interval in Millisekunden zwischen Verfügbarkeitschecks", + "machine_learning_availability_checks_timeout": "Anfragenzeitüberschreitung", + "machine_learning_availability_checks_timeout_description": "Zeitüberschreitung in Millisekunden für Verfügbarkeitschecks", "machine_learning_clip_model": "CLIP-Modell", "machine_learning_clip_model_description": "Der Name eines CLIP-Modells, welches hier aufgeführt ist. Beachte, dass du die Aufgabe \"Intelligente Suche\" für alle Bilder erneut ausführen musst, wenn du das Modell wechselst.", "machine_learning_duplicate_detection": "Duplikaterkennung", @@ -387,8 +395,6 @@ "admin_password": "Administrator Passwort", "administration": "Verwaltung", "advanced": "Erweitert", - "advanced_settings_beta_timeline_subtitle": "Probier die neue App-Erfahrung aus", - "advanced_settings_beta_timeline_title": "Beta-Timeline", "advanced_settings_enable_alternate_media_filter_subtitle": "Verwende diese Option, um Medien während der Synchronisierung nach anderen Kriterien zu filtern. Versuchen dies nur, wenn Probleme mit der Erkennung aller Alben durch die App auftreten.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTELL] Benutze alternativen Filter für Synchronisierung der Gerätealben", "advanced_settings_log_level_title": "Log-Level: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", "advanced_settings_proxy_headers_subtitle": "Definiere einen Proxy-Header, den Immich bei jeder Netzwerkanfrage mitschicken soll", "advanced_settings_proxy_headers_title": "Proxy-Headers", + "advanced_settings_readonly_mode_subtitle": "Aktiviert den schreibgeschützten Modus, in dem die Fotos nur angezeigt werden können. Funktionen wie das Auswählen mehrerer Bilder, das Teilen, das Übertragen und das Löschen sind deaktiviert. Aktivieren/Deaktiviere den schreibgeschützten Modus über den Benutzer-Avatar auf dem Hauptbildschirm", + "advanced_settings_readonly_mode_title": "Schreibgeschützter Modus", "advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.", "advanced_settings_self_signed_ssl_title": "Selbstsignierte SSL-Zertifikate erlauben", "advanced_settings_sync_remote_deletions_subtitle": "Automatisches Löschen oder Wiederherstellen einer Datei auf diesem Gerät, wenn diese Aktion im Web durchgeführt wird", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Bist du sicher, dass du {user} entfernen willst?", "album_search_not_found": "Keine Alben gefunden, die zur Suche passen", "album_share_no_users": "Es sieht so aus, als hättest du dieses Album mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", + "album_summary": "Album Zusammenfassung", "album_updated": "Album aktualisiert", "album_updated_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn ein freigegebenes Album neue Dateien enthält", "album_user_left": "{album} verlassen", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Abmelden", "app_settings": "App-Einstellungen", "appears_in": "Erscheint in", + "apply_count": "Anwenden ({count, number})", "archive": "Archiv", "archive_action_prompt": "{count} zum Archiv hinzugefügt", "archive_or_unarchive_photo": "Foto archivieren bzw. Archivierung aufheben", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Datei erfolgreich wiederhergestellt", "asset_skipped": "Übersprungen", "asset_skipped_in_trash": "Im Papierkorb", + "asset_trashed": "Datei Gelöscht", + "asset_troubleshoot": "Datei Fehlerbehebung", "asset_uploaded": "Hochgeladen", "asset_uploading": "Hochladen…", "asset_viewer_settings_subtitle": "Verwaltung der Einstellungen für die Fotoanzeige", @@ -500,7 +512,7 @@ "assets": "Dateien", "assets_added_count": "{count, plural, one {# Datei} other {# Dateien}} hinzugefügt", "assets_added_to_album_count": "{count, plural, one {# Datei} other {# Dateien}} zum Album hinzugefügt", - "assets_added_to_albums_count": "{assetTotal} Dateien zu {albumTotal} Alben hinzugefügt", + "assets_added_to_albums_count": "{assetTotal, plural, one {# Datei} other {# Dateien}} zu {albumTotal, plural, one {# Album} other {# Alben}} hinzugefügt", "assets_cannot_be_added_to_album_count": "{count, plural, one {Datei kann}other {Dateien können}} nicht zum Album hinzugefügt werden", "assets_cannot_be_added_to_albums": "{count, plural, one {Datei kann} other {Dateien können}} nicht zu den Alben hinzugefügt werden", "assets_count": "{count, plural, one {# Datei} other {# Dateien}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Automatische Diashow", "back": "Zurück", "back_close_deselect": "Zurück, Schließen oder Abwählen", + "background_backup_running_error": "Hintergrund Sicherung läuft, kann manuelle Sicherung nicht starten", "background_location_permission": "Hintergrund Standortfreigabe", "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu können, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann", + "background_options": "Hintergrund Optionen", "backup": "Sicherung", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({count})", "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Alben auswählen", "backup_album_selection_page_selection_info": "Information", "backup_album_selection_page_total_assets": "Elemente", + "backup_albums_sync": "Synchronisation von Alben beim Backup", "backup_all": "Alle", "backup_background_service_backup_failed_message": "Es trat ein Fehler bei der Sicherung auf. Erneuter Versuch…", "backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server hergestellt werden. Erneuter Versuch…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "Verwaltung der Upload-Einstellungen im Hintergrund und im Vordergrund", "backup_settings_subtitle": "Upload-Einstellungen verwalten", "backward": "Rückwärts", - "beta_sync": "Status der Beta-Synchronisierung", - "beta_sync_subtitle": "Verwalte das neue Synchronisierungssystem", "biometric_auth_enabled": "Biometrische Authentifizierung aktiviert", "biometric_locked_out": "Du bist von der biometrischen Authentifizierung ausgeschlossen", "biometric_no_options": "Keine biometrischen Optionen verfügbar", @@ -640,7 +653,7 @@ "change_description": "Beschreibung anpassen", "change_display_order": "Anzeigereihenfolge ändern", "change_expiration_time": "Verfallszeitpunkt ändern", - "change_location": "Ort ändern", + "change_location": "Standort ändern", "change_name": "Name ändern", "change_name_successfully": "Name wurde erfolgreich geändert", "change_password": "Passwort ändern", @@ -653,6 +666,8 @@ "change_pin_code": "PIN Code ändern", "change_your_password": "Ändere dein Passwort", "changed_visibility_successfully": "Die Sichtbarkeit wurde erfolgreich geändert", + "charging": "Aufladen", + "charging_requirement_mobile_backup": "Backup im Hintergrund erfordert Aufladen des Geräts", "check_corrupt_asset_backup": "Auf beschädigte Asset-Backups überprüfen", "check_corrupt_asset_backup_button": "Überprüfung durchführen", "check_corrupt_asset_backup_description": "Führe diese Prüfung nur mit aktivierten WLAN durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.", @@ -703,7 +718,7 @@ "control_bottom_app_bar_create_new_album": "Neues Album erstellen", "control_bottom_app_bar_delete_from_immich": "Aus Immich löschen", "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_location": "Standort bearbeiten", "control_bottom_app_bar_edit_time": "Datum und Uhrzeit bearbeiten", "control_bottom_app_bar_share_link": "Link teilen", "control_bottom_app_bar_share_to": "Teilen mit", @@ -739,6 +754,7 @@ "create_user": "Nutzer erstellen", "created": "Erstellt", "created_at": "Erstellt", + "creating_linked_albums": "Erstelle verknüpfte Alben...", "crop": "Zuschneiden", "curated_object_page_title": "Dinge", "current_device": "Aktuelles Gerät", @@ -859,7 +875,7 @@ "edit_link": "Link bearbeiten", "edit_location": "Standort bearbeiten", "edit_location_action_prompt": "{count} Geolokationen angepasst", - "edit_location_dialog_title": "Ort bearbeiten", + "edit_location_dialog_title": "Standort bearbeiten", "edit_name": "Name bearbeiten", "edit_people": "Personen bearbeiten", "edit_tag": "Tag bearbeiten", @@ -888,7 +904,9 @@ "error": "Fehler", "error_change_sort_album": "Ändern der Anzeigereihenfolge fehlgeschlagen", "error_delete_face": "Fehler beim Löschen des Gesichts", + "error_getting_places": "Fehler beim Abrufen der Orte", "error_loading_image": "Fehler beim Laden des Bildes", + "error_loading_partners": "Fehler beim Laden der Partner: {error}", "error_saving_image": "Fehler: {error}", "error_tag_face_bounding_box": "Fehler beim Markieren des Gesichts - Begrenzungen können nicht abgerufen werden", "error_title": "Fehler - Etwas ist schief gelaufen", @@ -944,7 +962,7 @@ "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_location": "Standort kann nicht verändert werden", "unable_to_change_password": "Passwort konnte nicht geändert werden", "unable_to_change_visibility": "Sichtbarkeit von {count, plural, one {einer Person} other {# Personen}} konnte nicht geändert werden", "unable_to_complete_oauth_login": "OAuth-Anmeldung konnte nicht abgeschlossen werden", @@ -1008,7 +1026,7 @@ "unable_to_update_album_cover": "Album-Cover konnte nicht aktualisiert werden", "unable_to_update_album_info": "Album-Info konnte nicht aktualisiert werden", "unable_to_update_library": "Die Bibliothek konnte nicht aktualisiert werden", - "unable_to_update_location": "Der Ort konnte nicht aktualisiert werden", + "unable_to_update_location": "Der Standort konnte nicht aktualisiert werden", "unable_to_update_settings": "Die Einstellungen konnten nicht aktualisiert werden", "unable_to_update_timeline_display_status": "Status der Zeitleistenanzeige konnte nicht aktualisiert werden", "unable_to_update_user": "Der Nutzer konnte nicht aktualisiert werden", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "Keine favorisierten Inhalte gefunden", "feature_photo_updated": "Profilbild aktualisiert", "features": "Funktionen", + "features_in_development": "Feature in Entwicklung", "features_setting_description": "Funktionen der App verwalten", "file_name": "Dateiname", "file_name_or_extension": "Dateiname oder -erweiterung", @@ -1073,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Diese Funktion lädt externe Quellen von Google, um zu funktionieren.", "general": "Allgemein", + "geolocation_instruction_location": "Klicke auf eine Datei mit GPS Koordinaten um diesen Standort zu verwenden oder wähle einen Standort direkt auf der Karte", "get_help": "Hilfe erhalten", "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist", "getting_started": "Erste Schritte", "go_back": "Zurück", "go_to_folder": "Gehe zu Ordner", "go_to_search": "Zur Suche gehen", + "gps": "GPS", + "gps_missing": "Kein GPS", "grant_permission": "Erlaubnis gewähren", "group_albums_by": "Alben gruppieren nach...", "group_country": "Nach Land gruppieren", @@ -1184,8 +1206,9 @@ "language_search_hint": "Sprachen durchsuchen...", "language_setting_description": "Wähle deine bevorzugte Sprache", "large_files": "Große Dateien", + "last": "Letzte", "last_seen": "Zuletzt gesehen", - "latest_version": "Aktuellste Version", + "latest_version": "Aktuelle Version", "latitude": "Breitengrad", "leave": "Verlassen", "leave_album": "Album verlassen", @@ -1213,6 +1236,7 @@ "local": "Lokal", "local_asset_cast_failed": "Eine Datei, die nicht auf den Server hochgeladen wurde, kann nicht gecastet werden", "local_assets": "Lokale Dateien", + "local_media_summary": "Zusammenfassung der lokalen Medien", "local_network": "Lokales Netzwerk", "local_network_sheet_info": "Die App stellt über diese URL eine Verbindung zum Server her, wenn sie das angegebene WLAN-Netzwerk verwendet", "location_permission": "Standort Genehmigung", @@ -1224,6 +1248,7 @@ "location_picker_longitude_hint": "Längengrad eingeben", "lock": "Sperren", "locked_folder": "Gesperrter Ordner", + "log_detail_title": "Protokoll Details", "log_out": "Abmelden", "log_out_all_devices": "Alle Geräte abmelden", "logged_in_as": "Angemeldet als {user}", @@ -1254,6 +1279,7 @@ "login_password_changed_success": "Passwort erfolgreich geändert", "logout_all_device_confirmation": "Bist du sicher, dass du alle Geräte abmelden willst?", "logout_this_device_confirmation": "Bist du sicher, dass du dieses Gerät abmelden willst?", + "logs": "Protokolle", "longitude": "Längengrad", "look": "Erscheinungsbild", "loop_videos": "Loop-Videos", @@ -1261,6 +1287,7 @@ "main_branch_warning": "Du benutzt eine Entwicklungsversion. Wir empfehlen dringend, eine Release-Version zu verwenden!", "main_menu": "Hauptmenü", "make": "Marke", + "manage_geolocation": "Standort verwalten", "manage_shared_links": "Freigegebene Links verwalten", "manage_sharing_with_partners": "Gemeinsame Nutzung mit Partnern verwalten", "manage_the_app_settings": "App-Einstellungen verwalten", @@ -1295,6 +1322,7 @@ "mark_as_read": "Als gelesen markieren", "marked_all_as_read": "Alle als gelesen markiert", "matches": "Treffer", + "matching_assets": "Passende Dateien", "media_type": "Medientyp", "memories": "Erinnerungen", "memories_all_caught_up": "Alles aufgeholt", @@ -1333,8 +1361,9 @@ "my_albums": "Meine Alben", "name": "Name", "name_or_nickname": "Name oder Nickname", - "network_requirement_photos_upload": "Mobiles Datennetz verwenden, um Fotos zu sichern", - "network_requirement_videos_upload": "Mobiles Datennetz verwenden, um Videos zu sichern", + "network_requirement_photos_upload": "Mobile Daten verwenden, um Fotos zu sichern", + "network_requirement_videos_upload": "Mobile Daten verwenden, um Videos zu sichern", + "network_requirements": "Anforderungen ans Netzwerk", "network_requirements_updated": "Netzwerk-Abhängigkeiten haben sich geändert, Backup-Warteschlange wird zurückgesetzt", "networking_settings": "Netzwerk", "networking_subtitle": "Verwaltung von Server-Endpunkt-Einstellungen", @@ -1345,6 +1374,7 @@ "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_timeline": "Neue Zeitleiste", "new_user_created": "Neuer Benutzer wurde erstellt", "new_version_available": "NEUE VERSION VERFÜGBAR", "newest_first": "Neueste zuerst", @@ -1358,20 +1388,25 @@ "no_assets_message": "KLICKE, UM DEIN ERSTES FOTO HOCHZULADEN", "no_assets_to_show": "Keine Vorschau vorhanden", "no_cast_devices_found": "Keine Geräte zum Übertragen gefunden", + "no_checksum_local": "Prüfsumme nicht verfügbar - kann lokale Datei/en nicht laden", + "no_checksum_remote": "Prüfsumme nicht verfügbar - kann entfernte Datei/en nicht laden", "no_duplicates_found": "Es wurden keine Duplikate gefunden.", "no_exif_info_available": "Keine EXIF-Informationen vorhanden", "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_local_assets_found": "Keine lokale Datei mit dieser Prüfsumme gefunden", "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", "no_places": "Keine Orte", + "no_remote_assets_found": "Keine entfernten Dateien mit dieser Prüfsumme gefunden", "no_results": "Keine Ergebnisse", "no_results_description": "Versuche es mit einem Synonym oder einem allgemeineren Stichwort", "no_shared_albums_message": "Erstelle ein Album, um Fotos und Videos mit Personen in deinem Netzwerk zu teilen", "no_uploads_in_progress": "Kein Upload in Bearbeitung", + "not_available": "N/A", "not_in_any_album": "In keinem Album", "not_selected": "Nicht ausgewählt", "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den", @@ -1406,6 +1441,8 @@ "open_the_search_filters": "Die Suchfilter öffnen", "options": "Optionen", "or": "oder", + "organize_into_albums": "In Alben organisieren", + "organize_into_albums_description": "Aktuelle Synchronisationseinstellungen verwenden, um existierende Fotos in Alben zu laden", "organize_your_library": "Organisiere deine Bibliothek", "original": "Original", "other": "Sonstiges", @@ -1467,7 +1504,7 @@ "person": "Person", "person_age_months": "{months, plural, one {# month} other {# months}} alt", "person_age_year_months": "1 Jahr, {months, plural, one {# month} other {# months}} alt", - "person_age_years": "{years, plural, other {# years}} alt", + "person_age_years": "{years, plural, one {# Jahr} other {# Jahre}} alt", "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.", @@ -1491,6 +1528,7 @@ "port": "Port", "preferences_settings_subtitle": "App-Einstellungen verwalten", "preferences_settings_title": "Voreinstellungen", + "preparing": "Vorbereiten", "preset": "Voreinstellung", "preview": "Vorschau", "previous": "Vorherige", @@ -1507,6 +1545,7 @@ "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_readonly_mode": "Schreibgeschützter Modus aktiviert. Halte das Benutzer-Avatar-Symbol gedrückt, um den Modus zu verlassen.", "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}", @@ -1532,19 +1571,20 @@ "purchase_license_subtitle": "Kaufe Immich, um die fortlaufende Entwicklung zu unterstützen", "purchase_lifetime_description": "Lebenslange Gültigkeit", "purchase_option_title": "KAUFOPTIONEN", - "purchase_panel_info_1": "Die Entwicklung von Immich erfordert viel Zeit und Mühe, und wir haben Vollzeit-Entwickler, die daran arbeiten es möglichst perfekt zu machen. Unser Ziel ist es, dass Open-Source-Software und moralische Geschäftsmethoden zu einer nachhaltigen Einkommensquelle für Entwickler werden und ein datenschutzfreundliches Ökosystem mit echten Alternativen zu ausbeuterischen Cloud-Diensten geschaffen wird.", + "purchase_panel_info_1": "Die Entwicklung von Immich erfordert viel Zeit und Mühe und wir haben Vollzeit-Entwickler, die daran arbeiten Immich möglichst perfekt zu machen. Unser Ziel ist es, Open-Source-Software und ethische Geschäftspraktiken zu einer verlässlichen Einkommensquelle für Entwickler zu machen und ein datenschutzfreundliches Ökosystem mit echten Alternativen zu ausbeuterischen Cloud-Diensten zu schaffen.", "purchase_panel_info_2": "Weil wir uns dagegen entschieden haben, eine Bezahlschranke einzusetzen, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um die Entwicklung von Immich zu unterstützen.", "purchase_panel_title": "Das Projekt unterstützen", "purchase_per_server": "Pro Server", "purchase_per_user": "Pro Benutzer", "purchase_remove_product_key": "Produktschlüssel entfernen", - "purchase_remove_product_key_prompt": "Sicher, dass der Produktschlüssel entfernt werden soll?", + "purchase_remove_product_key_prompt": "Bist Du sicher, dass der Produktschlüssel entfernt werden soll?", "purchase_remove_server_product_key": "Server-Produktschlüssel entfernen", "purchase_remove_server_product_key_prompt": "Sicher, dass der Server-Produktschlüssel entfernt werden soll?", "purchase_server_description_1": "Für den gesamten Server", "purchase_server_description_2": "Unterstützerstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Der Server-Produktschlüssel wird durch den Administrator verwaltet", + "query_asset_id": "Datei-ID abfragen", "queue_status": "Warteschlange {count}/{total}", "rating": "Bewertung", "rating_clear": "Bewertung löschen", @@ -1552,6 +1592,9 @@ "rating_description": "Stellt die EXIF-Bewertung im Informationsbereich dar", "reaction_options": "Reaktionsmöglichkeiten", "read_changelog": "Changelog lesen", + "readonly_mode_disabled": "Schreibgeschützter Modus deaktiviert", + "readonly_mode_enabled": "Schreibgeschützter Modus aktiviert", + "ready_for_upload": "Bereit zum Hochladen", "reassign": "Neu zuweisen", "reassigned_assets_to_existing_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} {name, select, null {einer vorhandenen Person} other {{name}}} zugewiesen", "reassigned_assets_to_new_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} einer neuen Person zugewiesen", @@ -1576,6 +1619,7 @@ "regenerating_thumbnails": "Miniaturansichten werden neu erstellt", "remote": "Server", "remote_assets": "Server-Dateien", + "remote_media_summary": "Zusammenfassung der entfernten Medien", "remove": "Entfernen", "remove_assets_album_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} aus dem Album entfernen willst?", "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?", @@ -1628,6 +1672,7 @@ "restore_user": "Nutzer wiederherstellen", "restored_asset": "Datei wiederhergestellt", "resume": "Fortsetzen", + "resume_paused_jobs": "{count, plural, one {# Aufgabe fortsetzen } other {# Aufgaben fortsetzen}}", "retry_upload": "Upload wiederholen", "review_duplicates": "Duplikate überprüfen", "review_large_files": "Große Dateien überprüfen", @@ -1721,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden", "selected": "Ausgewählt", "selected_count": "{count, plural, other {# ausgewählt}}", + "selected_gps_coordinates": "Ausgewählte GPS-Koordinaten", "send_message": "Nachricht senden", "send_welcome_email": "Begrüssungsmail senden", "server_endpoint": "Server-Endpunkt", @@ -1849,6 +1895,7 @@ "show_slideshow_transition": "Slideshow-Übergang anzeigen", "show_supporter_badge": "Unterstützerabzeichen", "show_supporter_badge_description": "Zeige Unterstützerabzeichen", + "show_text_search_menu": "Zeige Menü für Textsuche", "shuffle": "Durchmischen", "sidebar": "Seitenleiste", "sidebar_display_description": "Zeige einen Link zu der Ansicht in der Seitenleiste an", @@ -1879,6 +1926,7 @@ "stacktrace": "Stapelaufgaben", "start": "Starten", "start_date": "Anfangsdatum", + "start_date_before_end_date": "Anfangsdatum muss vor dem Enddatum liegen", "state": "Bundesland / Provinz", "status": "Status", "stop_casting": "Übertragung stoppen", @@ -1903,6 +1951,8 @@ "sync_albums_manual_subtitle": "Synchronisiere alle hochgeladenen Videos und Fotos in die ausgewählten Backup-Alben", "sync_local": "Lokal synchronisieren", "sync_remote": "mit Server synchronisieren", + "sync_status": "Synchronisierungstatus", + "sync_status_subtitle": "Synchronisierungssystem anzeigen und bearbeiten", "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", @@ -1940,7 +1990,9 @@ "to_change_password": "Passwort ändern", "to_favorite": "Zu Favoriten hinzufügen", "to_login": "Anmelden", + "to_multi_select": "zur Mehrfachauswahl", "to_parent": "Gehe zum Übergeordneten", + "to_select": "zum Auswählen", "to_trash": "In den Papierkorb verschieben", "toggle_settings": "Einstellungen umschalten", "total": "Gesamt", @@ -1960,6 +2012,7 @@ "trash_page_select_assets_btn": "Elemente auswählen", "trash_page_title": "Papierkorb ({count})", "trashed_items_will_be_permanently_deleted_after": "Gelöschte Objekte werden nach {days, plural, one {# Tag} other {# Tagen}} endgültig gelöscht.", + "troubleshoot": "Fehler beheben", "type": "Typ", "unable_to_change_pin_code": "PIN Code konnte nicht geändert werden", "unable_to_setup_pin_code": "PIN Code konnte nicht festgelegt werden", @@ -1990,6 +2043,7 @@ "unstacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} entstapelt", "untagged": "Ohne Tag", "up_next": "Weiter", + "update_location_action_prompt": "Aktualsiere den Ort von {count} ausgewählten Dateien mit:", "updated_at": "Aktualisiert", "updated_password": "Passwort aktualisiert", "upload": "Hochladen", @@ -2056,6 +2110,7 @@ "view_next_asset": "Nächste Datei anzeigen", "view_previous_asset": "Vorherige Datei anzeigen", "view_qr_code": "QR code anzeigen", + "view_similar_photos": "Zeige ähnliche Fotos an", "view_stack": "Stapel anzeigen", "view_user": "Benutzer anzeigen", "viewer_remove_from_stack": "Aus Stapel entfernen", @@ -2074,5 +2129,6 @@ "yes": "Ja", "you_dont_have_any_shared_links": "Du hast keine geteilten Links", "your_wifi_name": "Dein WLAN-Name", - "zoom_image": "Bild vergrößern" + "zoom_image": "Bild vergrößern", + "zoom_to_bounds": "In die Grenzen zoomen" } diff --git a/i18n/el.json b/i18n/el.json index e0d298f507..6e2f543c1f 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -123,6 +123,13 @@ "logging_enable_description": "Ενεργοποίηση καταγραφής συμβάντων", "logging_level_description": "Το επίπεδο καταγραφής συμβάντων που θα εφαρμοστεί, όταν αυτή είναι ενεργοποιημένη.", "logging_settings": "Καταγραφή Συμβάντων", + "machine_learning_availability_checks": "Έλεγχοι διαθεσιμότητας", + "machine_learning_availability_checks_description": "Αυτόματος ανίχνευση και προτίμηση διαθέσιμων διακομιστών μηχανικής μάθησης", + "machine_learning_availability_checks_enabled": "Ενεργοποίηση ελέγχων διαθεσιμότητας", + "machine_learning_availability_checks_interval": "Διάστημα ελέγχου", + "machine_learning_availability_checks_interval_description": "Διάστημα σε χιλιοστά δευτερολέπτου μεταξύ των ελέγχων διαθεσιμότητας", + "machine_learning_availability_checks_timeout": "Αίτημα χρονικού ορίου λήξης", + "machine_learning_availability_checks_timeout_description": "Χρονικό όριο σε χιλιοστά δευτερολέπτου για ελέγχους διαθεσιμότητας", "machine_learning_clip_model": "Μοντέλο CLIP", "machine_learning_clip_model_description": "Το όνομα ενός μοντέλου CLIP που αναφέρεται εδώ. Σημειώστε ότι πρέπει να επανεκτελέσετε την εργασία 'Έξυπνη Αναζήτηση' για όλες τις εικόνες μετά την αλλαγή μοντέλου.", "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων", @@ -387,8 +394,6 @@ "admin_password": "Κωδικός πρόσβασης Διαχειριστή", "administration": "Διαχείριση", "advanced": "Για προχωρημένους", - "advanced_settings_beta_timeline_subtitle": "Δοκίμασε τη νέα εμπειρία της εφαρμογής", - "advanced_settings_beta_timeline_title": "Δοκιμαστικό χρονολόγιο", "advanced_settings_enable_alternate_media_filter_subtitle": "Χρησιμοποιήστε αυτήν την επιλογή για να φιλτράρετε τα μέσα ενημέρωσης κατά τον συγχρονισμό με βάση εναλλακτικά κριτήρια. Δοκιμάστε αυτή τη δυνατότητα μόνο αν έχετε προβλήματα με την εφαρμογή που εντοπίζει όλα τα άλμπουμ.", "advanced_settings_enable_alternate_media_filter_title": "[ΠΕΙΡΑΜΑΤΙΚΟ] Χρήση εναλλακτικού φίλτρου συγχρονισμού άλμπουμ συσκευής", "advanced_settings_log_level_title": "Επίπεδο σύνδεσης: {level}", @@ -396,6 +401,8 @@ "advanced_settings_prefer_remote_title": "Προτίμηση απομακρυσμένων εικόνων", "advanced_settings_proxy_headers_subtitle": "Καθορισμός κεφαλίδων διακομιστή μεσολάβησης που το Immich πρέπει να στέλνει με κάθε αίτημα δικτύου", "advanced_settings_proxy_headers_title": "Κεφαλίδες διακομιστή μεσολάβησης", + "advanced_settings_readonly_mode_subtitle": "Ενεργοποιεί τη λειτουργία μόνο-για-ανάγνωση, όπου οι φωτογραφίες μπορούν μόνο να προβληθούν. Ενέργειες όπως επιλογή πολλών εικόνων, κοινή χρήση, αποστολή (casting) και διαγραφή είναι απενεργοποιημένες. Η ενεργοποίηση/απενεργοποίηση της λειτουργίας μόνο-για-ανάγνωση γίνεται μέσω της εικόνας του χρήστη από την κεντρική οθόνη", + "advanced_settings_readonly_mode_title": "Λειτουργία μόνο-για-ανάγνωση", "advanced_settings_self_signed_ssl_subtitle": "Παρακάμπτει τον έλεγχο πιστοποιητικού SSL του διακομιστή. Απαραίτητο για αυτο-υπογεγραμμένα πιστοποιητικά.", "advanced_settings_self_signed_ssl_title": "Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά SSL", "advanced_settings_sync_remote_deletions_subtitle": "Αυτόματη διαγραφή ή επαναφορά ενός περιουσιακού στοιχείου σε αυτή τη συσκευή, όταν η ενέργεια αυτή πραγματοποιείται στο διαδίκτυο", @@ -423,6 +430,7 @@ "album_remove_user_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον/την {user};", "album_search_not_found": "Δε βρέθηκαν άλμπουμ που να ταιριάζουν με την αναζήτησή σας", "album_share_no_users": "Φαίνεται ότι έχετε κοινοποιήσει αυτό το άλμπουμ σε όλους τους χρήστες ή δεν έχετε χρήστες για να το κοινοποιήσετε.", + "album_summary": "Περίληψη άλμπουμ", "album_updated": "Το άλμπουμ, ενημερώθηκε", "album_updated_setting_description": "Λάβετε ειδοποίηση μέσω email όταν ένα κοινόχρηστο άλμπουμ έχει νέα αρχεία", "album_user_left": "Αποχωρήσατε από το {album}", @@ -461,6 +469,7 @@ "app_bar_signout_dialog_title": "Αποσύνδεση", "app_settings": "Ρυθμίσεις εφαρμογής", "appears_in": "Εμφανίζεται σε", + "apply_count": "Εφαρμογή ({count, number})", "archive": "Αρχείο", "archive_action_prompt": "Προστέθηκαν {count} στο Αρχείο", "archive_or_unarchive_photo": "Αρχειοθέτηση ή αποαρχειοθέτηση φωτογραφίας", @@ -493,6 +502,8 @@ "asset_restored_successfully": "Το στοιχείο αποκαταστάθηκε με επιτυχία", "asset_skipped": "Παραλείφθηκε", "asset_skipped_in_trash": "Στον κάδο απορριμμάτων", + "asset_trashed": "Το στοιχείο διαγράφηκε", + "asset_troubleshoot": "Αντιμετώπιση προβλήματος στοιχείου", "asset_uploaded": "Ανεβάστηκε", "asset_uploading": "Ανεβάζεται…", "asset_viewer_settings_subtitle": "Διαχείριση ρυθμίσεων προβολής συλλογής", @@ -500,7 +511,7 @@ "assets": "Αντικείμενα", "assets_added_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}}", "assets_added_to_album_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο άλμπουμ", - "assets_added_to_albums_count": "Προστέθηκε {assetTotal, plural, one {# στοιχείο} other {# στοιχεία}} στα {albumTotal} άλμπουμ", + "assets_added_to_albums_count": "Προστέθηκαν {assetTotal, plural, one {# αρχείο} other {# αρχεία}} σε {albumTotal, plural, one {# άλμπουμ} other {# άλμπουμ}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Στοιχείο} other {Στοιχεία}} δεν μπορούν να προστεθούν στο άλμπουμ", "assets_cannot_be_added_to_albums": "Δεν μπορεί να προστεθεί κανένα {count, plural, one {στοιχείο} other {στοιχεία}} σε κανένα από τα άλμπουμ", "assets_count": "{count, plural, one {# αρχείο} other {# αρχεία}}", @@ -526,8 +537,10 @@ "autoplay_slideshow": "Αυτόματη αναπαραγωγή παρουσίασης", "back": "Πίσω", "back_close_deselect": "Πίσω, κλείσιμο ή αποεπιλογή", + "background_backup_running_error": "Η δημιουργία αντιγράφων ασφάλειας στο παρασκήνιο εκτελείται ήδη, δεν μπορεί να ξεκινήσετε χειροκίνητο αντίγραφο ασφάλειας", "background_location_permission": "Άδεια τοποθεσίας στο παρασκήνιο", "background_location_permission_content": "Το Immich για να μπορεί να αλλάζει δίκτυα όταν τρέχει στο παρασκήνιο, πρέπει *πάντα* να έχει πρόσβαση στην ακριβή τοποθεσία ώστε η εφαρμογή να μπορεί να διαβάζει το όνομα του δικτύου Wi-Fi", + "background_options": "Επιλογές παρασκηνίου", "backup": "Αντίγραφο ασφαλείας", "backup_album_selection_page_albums_device": "Άλμπουμ στη συσκευή ({count})", "backup_album_selection_page_albums_tap": "Πάτημα για συμπερίληψη, διπλό πάτημα για εξαίρεση", @@ -535,6 +548,7 @@ "backup_album_selection_page_select_albums": "Επιλογή άλμπουμ", "backup_album_selection_page_selection_info": "Πληροφορίες επιλογής", "backup_album_selection_page_total_assets": "Συνολικά μοναδικά στοιχεία", + "backup_albums_sync": "Συγχρονισμός αντιγράφων ασφαλείας άλμπουμ", "backup_all": "Όλα", "backup_background_service_backup_failed_message": "Αποτυχία δημιουργίας αντιγράφων ασφαλείας. Επανάληψη…", "backup_background_service_connection_failed_message": "Αποτυχία σύνδεσης με το διακομιστή. Επανάληψη…", @@ -594,8 +608,6 @@ "backup_setting_subtitle": "Διαχείριση ρυθμίσεων μεταφόρτωσης στο παρασκήνιο και στο προσκήνιο", "backup_settings_subtitle": "Διαχείριση των ρυθμίσεων μεταφόρτωσης", "backward": "Προς τα πίσω", - "beta_sync": "Κατάσταση Συγχρονισμού Beta (δοκιμαστική)", - "beta_sync_subtitle": "Διαχείριση του νέου συστήματος συγχρονισμού", "biometric_auth_enabled": "Βιομετρική ταυτοποίηση ενεργοποιήθηκε", "biometric_locked_out": "Είστε κλειδωμένοι εκτός της βιομετρικής ταυτοποίησης", "biometric_no_options": "Δεν υπάρχουν διαθέσιμοι τρόποι βιομετρικής ταυτοποίησης", @@ -653,6 +665,8 @@ "change_pin_code": "Αλλαγή κωδικού PIN", "change_your_password": "Αλλάξτε τον κωδικό σας", "changed_visibility_successfully": "Η προβολή, άλλαξε με επιτυχία", + "charging": "Φόρτιση", + "charging_requirement_mobile_backup": "Η δημιουργία αντιγράφων ασφάλειας στο παρασκήνιο απαιτεί η συσκευή να φορτίζει", "check_corrupt_asset_backup": "Έλεγχος για κατεστραμμένα αντίγραφα ασφαλείας στοιχείων", "check_corrupt_asset_backup_button": "Εκτέλεση ελέγχου", "check_corrupt_asset_backup_description": "Εκτέλεσε αυτόν τον έλεγχο μόνο μέσω Wi-Fi και αφού έχουν αποθηκευτεί όλα τα αντίγραφα ασφαλείας των στοιχείων. Η διαδικασία μπορεί να διαρκέσει μερικά λεπτά.", @@ -739,6 +753,7 @@ "create_user": "Δημιουργία χρήστη", "created": "Δημιουργήθηκε", "created_at": "Δημιουργήθηκε", + "creating_linked_albums": "Δημιουργία συνδεδεμένων άλμπουμ...", "crop": "Αποκοπή", "curated_object_page_title": "Πράγματα", "current_device": "Τρέχουσα συσκευή", @@ -888,7 +903,9 @@ "error": "Σφάλμα", "error_change_sort_album": "Απέτυχε η αλλαγή σειράς του άλμπουμ", "error_delete_face": "Σφάλμα διαγραφής προσώπου από το στοιχείο", + "error_getting_places": "Σφάλμα κατά την ανάκτηση τοποθεσιών", "error_loading_image": "Σφάλμα κατά τη φόρτωση της εικόνας", + "error_loading_partners": "Σφάλμα κατά τη φόρτωση συνεργατών: {error}", "error_saving_image": "Σφάλμα: {error}", "error_tag_face_bounding_box": "Σφάλμα επισήμανσης προσώπου - δεν μπορούν να ληφθούν οι συντεταγμένες του πλαισίου οριοθέτησης", "error_title": "Σφάλμα - Κάτι πήγε στραβά", @@ -1053,6 +1070,7 @@ "favorites_page_no_favorites": "Δεν βρέθηκαν αγαπημένα στοιχεία", "feature_photo_updated": "Η φωτογραφία προβολής ενημερώθηκε", "features": "Χαρακτηριστικά", + "features_in_development": "Λειτουργίες υπό Ανάπτυξη", "features_setting_description": "Διαχειριστείτε τα χαρακτηριστικά της εφαρμογής", "file_name": "Όνομα αρχείου", "file_name_or_extension": "Όνομα αρχείου ή επέκταση", @@ -1073,12 +1091,15 @@ "gcast_enabled": "Μετάδοση περιεχομένου Google Cast", "gcast_enabled_description": "Αυτό το χαρακτηριστικό φορτώνει εξωτερικούς πόρους από τη Google για να λειτουργήσει.", "general": "Γενικά", + "geolocation_instruction_location": "Κάνε κλικ σε ένα στοιχείο με συντεταγμένες GPS για να χρησιμοποιήσεις την τοποθεσία του, ή επίλεξε απευθείας μια τοποθεσία από τον χάρτη", "get_help": "Ζητήστε βοήθεια", "get_wifiname_error": "Δεν ήταν δυνατή η λήψη του ονόματος Wi-Fi. Βεβαιωθείτε ότι έχετε δώσει τις απαραίτητες άδειες και ότι είστε συνδεδεμένοι σε δίκτυο Wi-Fi", "getting_started": "Ξεκινώντας", "go_back": "Πηγαίνετε πίσω", "go_to_folder": "Μετάβαση στο φάκελο", "go_to_search": "Πηγαίνετε στην αναζήτηση", + "gps": "GPS", + "gps_missing": "Χωρίς GPS", "grant_permission": "Επιτρέψτε την άδεια", "group_albums_by": "Ομαδοποίηση άλμπουμ κατά...", "group_country": "Ομαδοποίηση κατά χώρα", @@ -1214,6 +1235,7 @@ "local": "Τοπικά", "local_asset_cast_failed": "Αδυναμία μετάδοσης στοιχείου που δεν έχει ανέβει στον διακομιστή", "local_assets": "Τοπικά στοιχεία", + "local_media_summary": "Περίληψη τοπικών πολυμέσων", "local_network": "Τοπικό δίκτυο", "local_network_sheet_info": "Η εφαρμογή θα συνδεθεί με τον διακομιστή μέσω αυτού του URL όταν χρησιμοποιείται το καθορισμένο δίκτυο Wi-Fi", "location_permission": "Άδεια τοποθεσίας", @@ -1225,6 +1247,7 @@ "location_picker_longitude_hint": "Εισαγάγετε εδώ το γεωγραφικό σας μήκος", "lock": "Κλείδωμα", "locked_folder": "Κλειδωμένος φάκελος", + "log_detail_title": "Λεπτομέρεια καταγραφής", "log_out": "Αποσύνδεση", "log_out_all_devices": "Αποσύνδεση από Όλες τις Συσκευές", "logged_in_as": "Συνδεδεμένος ως {user}", @@ -1255,6 +1278,7 @@ "login_password_changed_success": "Ο κωδικός πρόσβασης ενημερώθηκε με επιτυχία", "logout_all_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από όλες τις συσκευές;", "logout_this_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από αυτήν τη συσκευή;", + "logs": "Καταγραφές", "longitude": "Γεωγραφικό μήκος", "look": "Εμφάνιση", "loop_videos": "Επανάληψη βίντεο", @@ -1262,6 +1286,7 @@ "main_branch_warning": "Χρησιμοποιείτε μια έκδοση σε ανάπτυξη· συνιστούμε ανεπιφύλακτα τη χρήση μιας τελικής έκδοσης!", "main_menu": "Κύριο μενού", "make": "Κατασκευαστής", + "manage_geolocation": "Διαχείριση τοποθεσίας", "manage_shared_links": "Διαχείριση κοινόχρηστων συνδέσμων", "manage_sharing_with_partners": "Διαχειριστείτε την κοινή χρήση με συνεργάτες", "manage_the_app_settings": "Διαχειριστείτε τις ρυθμίσεις της εφαρμογής", @@ -1296,6 +1321,7 @@ "mark_as_read": "Επισήμανση ως αναγνωσμένο", "marked_all_as_read": "Όλα επισημάνθηκαν ως αναγνωσμένα", "matches": "Αντιστοιχίες", + "matching_assets": "Αντιστοιχία στοιχείων", "media_type": "Τύπος πολυμέσου", "memories": "Αναμνήσεις", "memories_all_caught_up": "Συγχρονισμένα", @@ -1336,6 +1362,7 @@ "name_or_nickname": "Όνομα ή ψευδώνυμο", "network_requirement_photos_upload": "Χρήση δεδομένων κινητής τηλεφωνίας για τη δημιουργία αντιγράφων ασφαλείας των φωτογραφιών", "network_requirement_videos_upload": "Χρήση δεδομένων κινητής τηλεφωνίας για τη δημιουργία αντιγράφων ασφαλείας των βίντεο", + "network_requirements": "Απαιτήσεις Δυκτίου", "network_requirements_updated": "Οι απαιτήσεις δικτύου άλλαξαν, γίνεται επαναφορά της ουράς αντιγράφων ασφαλείας", "networking_settings": "Δικτύωση", "networking_subtitle": "Διαχείριση ρυθμίσεων τελικών σημείων διακομιστή", @@ -1346,6 +1373,7 @@ "new_person": "Νέο άτομο", "new_pin_code": "Νέος κωδικός PIN", "new_pin_code_subtitle": "Αυτή είναι η πρώτη φορά που αποκτάτε πρόσβαση στον κλειδωμένο φάκελο. Δημιουργήστε έναν κωδικό PIN για ασφαλή πρόσβαση σε αυτή τη σελίδα", + "new_timeline": "Νέο Χρονολόγιο", "new_user_created": "Ο νέος χρήστης δημιουργήθηκε", "new_version_available": "ΔΙΑΘΕΣΙΜΗ ΝΕΑ ΕΚΔΟΣΗ", "newest_first": "Τα νεότερα πρώτα", @@ -1359,20 +1387,25 @@ "no_assets_message": "ΚΑΝΤΕ ΚΛΙΚ ΓΙΑ ΝΑ ΑΝΕΒΑΣΕΤΕ ΤΗΝ ΠΡΩΤΗ ΣΑΣ ΦΩΤΟΓΡΑΦΙΑ", "no_assets_to_show": "Δεν υπάρχουν στοιχεία προς εμφάνιση", "no_cast_devices_found": "Δε βρέθηκαν συσκευές μετάδοσης", + "no_checksum_local": "Δεν υπάρχει διαθέσιμο checksum για έλεγχο ακεραιότητας – δεν μπορούν να ανακτηθούν τα τοπικά στοιχεία", + "no_checksum_remote": "Δεν υπάρχει διαθέσιμο checksum για έλεγχο ακεραιότητας – δεν μπορούν να ανακτηθούν τα απομακρυσμένα στοιχεία", "no_duplicates_found": "Δεν βρέθηκαν διπλότυπα.", "no_exif_info_available": "Καμία πληροφορία exif διαθέσιμη", "no_explore_results_message": "Ανεβάστε περισσότερες φωτογραφίες για να περιηγηθείτε στη συλλογή σας.", "no_favorites_message": "Προσθέστε αγαπημένα για να βρείτε γρήγορα τις καλύτερες φωτογραφίες και τα βίντεό σας", "no_libraries_message": "Δημιουργήστε μια εξωτερική βιβλιοθήκη για να προβάλετε τις φωτογραφίες και τα βίντεό σας", + "no_local_assets_found": "Δεν βρέθηκαν τοπικά στοιχεία με αυτό το checksum", "no_locked_photos_message": "Οι φωτογραφίες και τα βίντεο στον κλειδωμένο φάκελο, είναι κρυμμένες και δεν θα εμφανίζονται κατά την περιήγηση ή την αναζήτηση στη βιβλιοθήκη σας.", "no_name": "Χωρίς Όνομα", "no_notifications": "Καμία ειδοποίηση", "no_people_found": "Δεν βρέθηκαν άτομα που να ταιριάζουν", "no_places": "Καμία τοποθεσία", + "no_remote_assets_found": "Δεν βρέθηκαν απομακρυσμένα στοιχεία με αυτό το checksum", "no_results": "Κανένα αποτέλεσμα", "no_results_description": "Δοκιμάστε ένα συνώνυμο ή πιο γενική λέξη-κλειδί", "no_shared_albums_message": "Δημιουργήστε ένα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας", "no_uploads_in_progress": "Καμία μεταφόρτωση σε εξέλιξη", + "not_available": "Μ/Δ (Μη Διαθέσιμο)", "not_in_any_album": "Σε κανένα άλμπουμ", "not_selected": "Δεν επιλέχθηκε", "note_apply_storage_label_to_previously_uploaded assets": "Σημείωση: Για να εφαρμόσετε την Ετικέτα Αποθήκευσης σε στοιχεία που έχουν μεταφορτωθεί προηγουμένως, εκτελέστε το", @@ -1407,6 +1440,8 @@ "open_the_search_filters": "Ανοίξτε τα φίλτρα αναζήτησης", "options": "Επιλογές", "or": "ή", + "organize_into_albums": "Οργάνωση σε άλμπουμ", + "organize_into_albums_description": "Τοποθετείστε τις υπάρχουσες φωτογραφίες σε άλμπουμ χρησιμοποιώντας τις τρέχουσες ρυθμίσεις συγχρονισμού", "organize_your_library": "Οργανώστε τη βιβλιοθήκη σας", "original": "πρωτότυπο", "other": "Άλλες", @@ -1492,6 +1527,7 @@ "port": "Θύρα", "preferences_settings_subtitle": "Διαχειριστείτε τις προτιμήσεις της εφαρμογής", "preferences_settings_title": "Προτιμήσεις", + "preparing": "Προετοιμασία", "preset": "Προκαθορισμένη ρύθμιση", "preview": "Προεπισκόπηση", "previous": "Προηγούμενο", @@ -1508,6 +1544,7 @@ "profile_drawer_client_out_of_date_minor": "Παρακαλώ ενημερώστε την εφαρμογή στην πιο πρόσφατη δευτερεύουσα έκδοση.", "profile_drawer_client_server_up_to_date": "Ο πελάτης και ο διακομιστής είναι ενημερωμένοι", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Η λειτουργία μόνο-για-ανάγνωση ενεργοποιήθηκε. Κρατήστε πατημένο το εικονίδιο του χρήστη για απενεργοποίηση.", "profile_drawer_server_out_of_date_major": "Παρακαλώ ενημερώστε τον διακομιστή στην πιο πρόσφατη κύρια έκδοση.", "profile_drawer_server_out_of_date_minor": "Παρακαλώ ενημερώστε τον διακομιστή στην πιο πρόσφατη δευτερεύουσα έκδοση.", "profile_image_of_user": "Εικόνα προφίλ του χρήστη {user}", @@ -1546,6 +1583,7 @@ "purchase_server_description_2": "Κατάσταση υποστηρικτή", "purchase_server_title": "Διακομιστής", "purchase_settings_server_activated": "Η διαχείριση του κλειδιού προϊόντος του διακομιστή γίνεται από τον διαχειριστή", + "query_asset_id": "Αναζήτηση ID Στοιχείου", "queue_status": "Τοποθέτηση στη ουρά {count} από {total}", "rating": "Αξιολόγηση με αστέρια", "rating_clear": "Εκκαθάριση αξιολόγησης", @@ -1553,6 +1591,9 @@ "rating_description": "Εμφάνιση της αξιολόγησης EXIF στον πίνακα πληροφοριών", "reaction_options": "Επιλογές αντίδρασης", "read_changelog": "Διαβάστε το Αρχείο Καταγραφής Αλλαγών", + "readonly_mode_disabled": "Η λειτουργία μόνο-για-ανάγνωση απενεργοποιήθηκε", + "readonly_mode_enabled": "Η λειτουργία μόνο-για-ανάγνωση ενεργοποιήθηκε", + "ready_for_upload": "Έτοιμο για μεταφόρτωση", "reassign": "Ανάθεση", "reassigned_assets_to_existing_person": "Η ανάθεση {count, plural, one {# αρχείου} other {# αρχείων}} στον/στην {name, select, null {έναν/μία υπάρχοντα/ουσα χρήστη} other {{name}}}", "reassigned_assets_to_new_person": "Η ανάθεση {count, plural, one {# αρχείου} other {# αρχείων}} σε νέο άτομο", @@ -1577,6 +1618,7 @@ "regenerating_thumbnails": "Οι μικρογραφίες αναγεννώνται", "remote": "Απομακρυσμένος", "remote_assets": "Απομακρυσμένα στοιχεία", + "remote_media_summary": "Περίληψη απομακρυσμένων πολυμέσων", "remove": "Αφαίρεση", "remove_assets_album_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε {count, plural, one {# στοιχείο} other {# στοιχεία}} από το άλμπουμ;", "remove_assets_shared_link_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε {count, plural, one {# στοιχείο} other {# στοιχεία}} από αυτόν τον κοινόχρηστο σύνδεσμο;", @@ -1629,6 +1671,7 @@ "restore_user": "Επαναφορά χρήστη", "restored_asset": "Ανακτήθηκε το αρχείο", "resume": "Συνέχιση", + "resume_paused_jobs": "Συνέχιση {count, plural, one {# σε παύση εργασία} other {# σε παύση εργασίες}}", "retry_upload": "Επανάληψη ανεβάσματος", "review_duplicates": "Προβολή διπλότυπων", "review_large_files": "Επισκόπηση μεγάλων αρχείων", @@ -1722,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Αποτυχία δημιουργίας άλπουμ", "selected": "Επιλεγμένοι", "selected_count": "{count, plural, other {# επιλεγμένοι}}", + "selected_gps_coordinates": "Επιλεγμένες συντεταγμένες GPS", "send_message": "Αποστολή μηνύματος", "send_welcome_email": "Αποστολή email καλωσορίσματος", "server_endpoint": "Τελικό σημείο Διακομιστή", @@ -1850,6 +1894,7 @@ "show_slideshow_transition": "Εμφάνιση μετάβασης παρουσίασης", "show_supporter_badge": "Σήμα υποστηρικτή", "show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή", + "show_text_search_menu": "Εμφάνιση μενού αναζήτησης κειμένου", "shuffle": "Ανάμειξη", "sidebar": "Πλαϊνή μπάρα", "sidebar_display_description": "Εμφάνιση συνδέσμου για προβολή στην πλαϊνή μπάρα", @@ -1880,6 +1925,7 @@ "stacktrace": "Καταγραφή στοίβας", "start": "Έναρξη", "start_date": "Από", + "start_date_before_end_date": "Η ημερομηνία έναρξης πρέπει να είναι πριν από την ημερομηνία λήξης", "state": "Νομός", "status": "Κατάσταση", "stop_casting": "Διακοπή μετάδοσης", @@ -1904,6 +1950,8 @@ "sync_albums_manual_subtitle": "Συγχρονίστε όλα τα μεταφορτωμένα βίντεο και φωτογραφίες με τα επιλεγμένα εφεδρικά άλμπουμ", "sync_local": "Τοπικός Συγχρονισμός", "sync_remote": "Απομακρυσμένος Συγχρονισμός", + "sync_status": "Κατάσταση συγχρονισμού", + "sync_status_subtitle": "Προβολή και διαχείριση του συστήματος συγχρονισμού", "sync_upload_album_setting_subtitle": "Δημιουργήστε και ανεβάστε τις φωτογραφίες και τα βίντεό σας στα επιλεγμένα άλμπουμ στο Immich", "tag": "Ετικέτα", "tag_assets": "Ετικετοποίηση στοιχείων", @@ -1941,7 +1989,9 @@ "to_change_password": "Αλλαγή κωδικού πρόσβασης", "to_favorite": "Αγαπημένο", "to_login": "Είσοδος", + "to_multi_select": "για πολλαπλή επιλογή", "to_parent": "Μεταβείτε στο γονικό φάκελο", + "to_select": "για επιλογή", "to_trash": "Κάδος απορριμμάτων", "toggle_settings": "Εναλλαγή ρυθμίσεων", "total": "Σύνολο", @@ -1961,6 +2011,7 @@ "trash_page_select_assets_btn": "Επιλέξτε στοιχεία", "trash_page_title": "Κάδος Απορριμμάτων ({count})", "trashed_items_will_be_permanently_deleted_after": "Τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων θα διαγραφούν οριστικά μετά από {days, plural, one {# ημέρα} other {# ημέρες}}.", + "troubleshoot": "Επίλυση προβλημάτων", "type": "Τύπος", "unable_to_change_pin_code": "Αδυναμία αλλαγής κωδικού PIN", "unable_to_setup_pin_code": "Αδυναμία ρύθμισης κωδικού PIN", @@ -1991,6 +2042,7 @@ "unstacked_assets_count": "Αποστοιβάξατε {count, plural, one {# στοιχείο} other {# στοιχεία}}", "untagged": "Χωρίς ετικέτα", "up_next": "Ακολουθεί", + "update_location_action_prompt": "Ενημέρωση τοποθεσίας για {count} επιλεγμένα στοιχεία με:", "updated_at": "Ενημερωμένο", "updated_password": "Ο κωδικός πρόσβασης ενημερώθηκε", "upload": "Μεταφόρτωση", @@ -2057,6 +2109,7 @@ "view_next_asset": "Προβολή επόμενου στοιχείου", "view_previous_asset": "Προβολή προηγούμενου στοιχείου", "view_qr_code": "Προβολή κωδικού QR", + "view_similar_photos": "Προβολή παρόμοιων φωτογραφιών", "view_stack": "Προβολή της στοίβας", "view_user": "Προβολή Χρήστη", "viewer_remove_from_stack": "Κατάργηση από τη Στοίβα", @@ -2075,5 +2128,6 @@ "yes": "Ναι", "you_dont_have_any_shared_links": "Δεν έχετε κοινόχρηστους συνδέσμους", "your_wifi_name": "Το όνομα του Wi-Fi σας", - "zoom_image": "Ζουμ Εικόνας" + "zoom_image": "Ζουμ Εικόνας", + "zoom_to_bounds": "Εστίαση στα όρια" } diff --git a/i18n/en.json b/i18n/en.json index f061cb1450..e86b56be85 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -28,6 +28,7 @@ "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_album_bottom_sheet_some_local_assets": "Some local assets could not be added to album", "add_to_album_toggle": "Toggle selection for {album}", "add_to_albums": "Add to albums", "add_to_albums_count": "Add to albums ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Enable logging", "logging_level_description": "When enabled, what log level to use.", "logging_settings": "Logging", + "machine_learning_availability_checks": "Availability checks", + "machine_learning_availability_checks_description": "Automatically detect and prefer available machine learning servers", + "machine_learning_availability_checks_enabled": "Enable availability checks", + "machine_learning_availability_checks_interval": "Check interval", + "machine_learning_availability_checks_interval_description": "Interval in milliseconds between availability checks", + "machine_learning_availability_checks_timeout": "Request timeout", + "machine_learning_availability_checks_timeout_description": "Timeout in milliseconds for availability checks", "machine_learning_clip_model": "CLIP model", "machine_learning_clip_model_description": "The name of a CLIP model listed here. Note that you must re-run the 'Smart Search' job for all images upon changing a model.", "machine_learning_duplicate_detection": "Duplicate Detection", @@ -387,8 +395,6 @@ "admin_password": "Admin Password", "administration": "Administration", "advanced": "Advanced", - "advanced_settings_beta_timeline_subtitle": "Try the new app experience", - "advanced_settings_beta_timeline_title": "Beta Timeline", "advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter", "advanced_settings_log_level_title": "Log level: {level}", @@ -396,6 +402,8 @@ "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_readonly_mode_subtitle": "Enables the read-only mode where the photos can be only viewed, things like selecting multiple images, sharing, casting, delete are all disabled. Enable/Disable read-only via user avatar from the main screen", + "advanced_settings_readonly_mode_title": "Read-only Mode", "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_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Are you sure you want to remove {user}?", "album_search_not_found": "No albums found matching your search", "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_summary": "Album summary", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", "album_user_left": "Left {album}", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Sign out", "app_settings": "App Settings", "appears_in": "Appears in", + "apply_count": "Apply ({count, number})", "archive": "Archive", "archive_action_prompt": "{count} added to Archive", "archive_or_unarchive_photo": "Archive or unarchive photo", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Asset restored successfully", "asset_skipped": "Skipped", "asset_skipped_in_trash": "In trash", + "asset_trashed": "Asset trashed", + "asset_troubleshoot": "Asset Troubleshoot", "asset_uploaded": "Uploaded", "asset_uploading": "Uploading…", "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Autoplay slideshow", "back": "Back", "back_close_deselect": "Back, close, or deselect", + "background_backup_running_error": "Background backup is currently running, cannot start manual backup", "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", + "background_options": "Background Options", "backup": "Backup", "backup_album_selection_page_albums_device": "Albums on device ({count})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -535,6 +549,7 @@ "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_albums_sync": "Backup albums synchronization", "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…", @@ -584,6 +599,7 @@ "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_error_sync_failed": "Sync failed. Cannot process backup.", "backup_info_card_assets": "assets", "backup_manual_cancelled": "Cancelled", "backup_manual_in_progress": "Upload already in progress. Try after sometime", @@ -594,8 +610,6 @@ "backup_setting_subtitle": "Manage background and foreground upload settings", "backup_settings_subtitle": "Manage upload settings", "backward": "Backward", - "beta_sync": "Beta Sync Status", - "beta_sync_subtitle": "Manage the new sync system", "biometric_auth_enabled": "Biometric authentication enabled", "biometric_locked_out": "You are locked out of biometric authentication", "biometric_no_options": "No biometric options available", @@ -653,6 +667,8 @@ "change_pin_code": "Change PIN code", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", + "charging": "Charging", + "charging_requirement_mobile_backup": "Background backup requires the device to be charging", "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.", @@ -739,6 +755,7 @@ "create_user": "Create user", "created": "Created", "created_at": "Created", + "creating_linked_albums": "Creating linked albums...", "crop": "Crop", "curated_object_page_title": "Things", "current_device": "Current device", @@ -888,7 +905,9 @@ "error": "Error", "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Error deleting face from asset", + "error_getting_places": "Error getting places", "error_loading_image": "Error loading image", + "error_loading_partners": "Error loading partners: {error}", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_title": "Error - Something went wrong", @@ -1053,6 +1072,7 @@ "favorites_page_no_favorites": "No favorite assets found", "feature_photo_updated": "Feature photo updated", "features": "Features", + "features_in_development": "Features in Development", "features_setting_description": "Manage the app features", "file_name": "File name", "file_name_or_extension": "File name or extension", @@ -1073,12 +1093,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", + "geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map", "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", "getting_started": "Getting Started", "go_back": "Go back", "go_to_folder": "Go to folder", "go_to_search": "Go to search", + "gps": "GPS", + "gps_missing": "No GPS", "grant_permission": "Grant permission", "group_albums_by": "Group albums by...", "group_country": "Group by country", @@ -1214,6 +1237,7 @@ "local": "Local", "local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server", "local_assets": "Local Assets", + "local_media_summary": "Local Media Summary", "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", @@ -1225,6 +1249,7 @@ "location_picker_longitude_hint": "Enter your longitude here", "lock": "Lock", "locked_folder": "Locked Folder", + "log_detail_title": "Log Detail", "log_out": "Log out", "log_out_all_devices": "Log Out All Devices", "logged_in_as": "Logged in as {user}", @@ -1255,6 +1280,7 @@ "login_password_changed_success": "Password updated successfully", "logout_all_device_confirmation": "Are you sure you want to log out all devices?", "logout_this_device_confirmation": "Are you sure you want to log out this device?", + "logs": "Logs", "longitude": "Longitude", "look": "Look", "loop_videos": "Loop videos", @@ -1262,6 +1288,7 @@ "main_branch_warning": "You're using a development version; we strongly recommend using a release version!", "main_menu": "Main menu", "make": "Make", + "manage_geolocation": "Manage location", "manage_shared_links": "Manage shared links", "manage_sharing_with_partners": "Manage sharing with partners", "manage_the_app_settings": "Manage the app settings", @@ -1296,6 +1323,7 @@ "mark_as_read": "Mark as read", "marked_all_as_read": "Marked all as read", "matches": "Matches", + "matching_assets": "Matching Assets", "media_type": "Media type", "memories": "Memories", "memories_all_caught_up": "All caught up", @@ -1336,6 +1364,7 @@ "name_or_nickname": "Name or nickname", "network_requirement_photos_upload": "Use cellular data to backup photos", "network_requirement_videos_upload": "Use cellular data to backup videos", + "network_requirements": "Network Requirements", "network_requirements_updated": "Network requirements changed, resetting backup queue", "networking_settings": "Networking", "networking_subtitle": "Manage the server endpoint settings", @@ -1346,6 +1375,7 @@ "new_person": "New person", "new_pin_code": "New PIN code", "new_pin_code_subtitle": "This is your first time accessing the locked folder. Create a PIN code to securely access this page", + "new_timeline": "New Timeline", "new_user_created": "New user created", "new_version_available": "NEW VERSION AVAILABLE", "newest_first": "Newest first", @@ -1359,20 +1389,25 @@ "no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", "no_assets_to_show": "No assets to show", "no_cast_devices_found": "No cast devices found", + "no_checksum_local": "No checksum available - cannot fetch local assets", + "no_checksum_remote": "No checksum available - cannot fetch remote asset", "no_duplicates_found": "No duplicates were found.", "no_exif_info_available": "No exif info available", "no_explore_results_message": "Upload more photos to explore your collection.", "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_local_assets_found": "No local assets found with this checksum", "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", "no_places": "No places", + "no_remote_assets_found": "No remote assets found with this checksum", "no_results": "No results", "no_results_description": "Try a synonym or more general keyword", "no_shared_albums_message": "Create an album to share photos and videos with people in your network", "no_uploads_in_progress": "No uploads in progress", + "not_available": "N/A", "not_in_any_album": "Not in any album", "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", @@ -1407,6 +1442,8 @@ "open_the_search_filters": "Open the search filters", "options": "Options", "or": "or", + "organize_into_albums": "Organize into albums", + "organize_into_albums_description": "Put existing photos into albums using current sync settings", "organize_your_library": "Organize your library", "original": "original", "other": "Other", @@ -1492,6 +1529,7 @@ "port": "Port", "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", + "preparing": "Preparing", "preset": "Preset", "preview": "Preview", "previous": "Previous", @@ -1508,6 +1546,7 @@ "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_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.", "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": "Profile image of {user}", @@ -1546,6 +1585,7 @@ "purchase_server_description_2": "Supporter status", "purchase_server_title": "Server", "purchase_settings_server_activated": "The server product key is managed by the admin", + "query_asset_id": "Query Asset ID", "queue_status": "Queuing {count}/{total}", "rating": "Star rating", "rating_clear": "Clear rating", @@ -1553,6 +1593,9 @@ "rating_description": "Display the EXIF rating in the info panel", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", + "readonly_mode_disabled": "Read-only mode disabled", + "readonly_mode_enabled": "Read-only mode enabled", + "ready_for_upload": "Ready for upload", "reassign": "Reassign", "reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}", "reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person", @@ -1577,6 +1620,7 @@ "regenerating_thumbnails": "Regenerating thumbnails", "remote": "Remote", "remote_assets": "Remote Assets", + "remote_media_summary": "Remote Media Summary", "remove": "Remove", "remove_assets_album_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from the album?", "remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?", @@ -1629,6 +1673,7 @@ "restore_user": "Restore user", "restored_asset": "Restored asset", "resume": "Resume", + "resume_paused_jobs": "Resume {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Retry upload", "review_duplicates": "Review duplicates", "review_large_files": "Review large files", @@ -1722,6 +1767,7 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "selected": "Selected", "selected_count": "{count, plural, other {# selected}}", + "selected_gps_coordinates": "Selected GPS Coordinates", "send_message": "Send message", "send_welcome_email": "Send welcome email", "server_endpoint": "Server Endpoint", @@ -1850,6 +1896,7 @@ "show_slideshow_transition": "Show slideshow transition", "show_supporter_badge": "Supporter badge", "show_supporter_badge_description": "Show a supporter badge", + "show_text_search_menu": "Show text search menu", "shuffle": "Shuffle", "sidebar": "Sidebar", "sidebar_display_description": "Display a link to the view in the sidebar", @@ -1880,6 +1927,7 @@ "stacktrace": "Stacktrace", "start": "Start", "start_date": "Start date", + "start_date_before_end_date": "Start date must be before end date", "state": "State", "status": "Status", "stop_casting": "Stop casting", @@ -1904,6 +1952,8 @@ "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", "sync_local": "Sync Local", "sync_remote": "Sync Remote", + "sync_status": "Sync Status", + "sync_status_subtitle": "View and manage the sync system", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "Tag", "tag_assets": "Tag assets", @@ -1941,7 +1991,9 @@ "to_change_password": "Change password", "to_favorite": "Favorite", "to_login": "Login", + "to_multi_select": "to multi-select", "to_parent": "Go to parent", + "to_select": "to select", "to_trash": "Trash", "toggle_settings": "Toggle settings", "total": "Total", @@ -1961,6 +2013,7 @@ "trash_page_select_assets_btn": "Select assets", "trash_page_title": "Trash ({count})", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", + "troubleshoot": "Troubleshoot", "type": "Type", "unable_to_change_pin_code": "Unable to change PIN code", "unable_to_setup_pin_code": "Unable to setup PIN code", @@ -1991,6 +2044,7 @@ "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "untagged": "Untagged", "up_next": "Up next", + "update_location_action_prompt": "Update the location of {count} selected assets with:", "updated_at": "Updated", "updated_password": "Updated password", "upload": "Upload", @@ -2057,6 +2111,7 @@ "view_next_asset": "View next asset", "view_previous_asset": "View previous asset", "view_qr_code": "View QR code", + "view_similar_photos": "View similar photos", "view_stack": "View Stack", "view_user": "View User", "viewer_remove_from_stack": "Remove from Stack", @@ -2075,5 +2130,6 @@ "yes": "Yes", "you_dont_have_any_shared_links": "You don't have any shared links", "your_wifi_name": "Your Wi-Fi name", - "zoom_image": "Zoom Image" + "zoom_image": "Zoom Image", + "zoom_to_bounds": "Zoom to bounds" } diff --git a/i18n/es.json b/i18n/es.json index 50f7383390..d2cd6924c4 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -28,6 +28,10 @@ "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_album_bottom_sheet_some_local_assets": "Algunos recursos locales no se pudieron añadir al álbum", + "add_to_album_toggle": "Alternar selección para el {album}", + "add_to_albums": "Incluir en álbumes", + "add_to_albums_count": "Incluir en {count} álbumes", "add_to_shared_album": "Incluir en álbum compartido", "add_url": "Agregar URL", "added_to_archive": "Agregado al Archivado", @@ -35,7 +39,7 @@ "added_to_favorites_count": "Agregado {count, number} a favoritos", "admin": { "add_exclusion_pattern_description": "Agrega patrones de exclusión. Puedes utilizar los caracteres *, ** y ? (globbing). Ejemplos: para ignorar todos los archivos en cualquier directorio llamado \"Raw\", utiliza \"**/Raw/**\". Para ignorar todos los archivos que terminan en \".tif\", utiliza \"**/*.tif\". Para ignorar una ruta absoluta, utiliza \"/carpeta/a/ignorar/**\".", - "admin_user": "Usuario administrativo", + "admin_user": "Usuario administrador", "asset_offline_description": "Este recurso externo de la biblioteca ya no se encuentra en el disco y se ha movido a la papelera. Si el archivo se movió dentro de la biblioteca, comprueba la línea temporal para el nuevo recurso correspondiente. Para restaurar este recurso, asegúrate de que Immich puede acceder a la siguiente ruta de archivo y escanear la biblioteca.", "authentication_settings": "Parámetros de autenticación", "authentication_settings_description": "Gestionar contraseñas, OAuth y otros parámetros de autenticación", @@ -120,6 +124,13 @@ "logging_enable_description": "Habilitar registro", "logging_level_description": "Indica el nivel de registro a utilizar cuando está habilitado.", "logging_settings": "Registro", + "machine_learning_availability_checks": "Comprobaciones de disponibilidad", + "machine_learning_availability_checks_description": "Automáticamente detectar y preferir servidores de machine learning disponibles", + "machine_learning_availability_checks_enabled": "Habilitar comprobaciones de disponibilidad", + "machine_learning_availability_checks_interval": "Intervalo de comprobación", + "machine_learning_availability_checks_interval_description": "Intervalo en milisegundos entre las comprobaciones de disponibilidad", + "machine_learning_availability_checks_timeout": "Tiempo de espera de solicitud", + "machine_learning_availability_checks_timeout_description": "Tiempo de espera en milisegundos para comprobaciones de disponibilidad", "machine_learning_clip_model": "Modelo CLIP (Contrastive Language-Image Pre-Training)", "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado aquí. Tendrás que relanzar el trabajo 'Búsqueda Inteligente' para todos los elementos al cambiar de modelo.", "machine_learning_duplicate_detection": "Detección de duplicados", @@ -164,8 +175,8 @@ "map_settings": "Mapa", "map_settings_description": "Administrar la configuración del mapa", "map_style_description": "Dirección URL a un tema de mapa (style.json)", - "memory_cleanup_job": "Limpieza de memoria", - "memory_generate_job": "Generación de memoria", + "memory_cleanup_job": "Limpieza de recuerdos", + "memory_generate_job": "Generación de recuerdos", "metadata_extraction_job": "Extracción de metadatos", "metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras y resolución", "metadata_faces_import_setting": "Activar importación de caras", @@ -229,7 +240,7 @@ "oauth_storage_quota_claim_description": "Fijar la cuota de almacenamiento del usuario automáticamente al valor solicitado.", "oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)", "oauth_storage_quota_default_description": "Cuota (en GiB) que se usará cuando no se solicite un valor específico.", - "oauth_timeout": "Límite de tiempo para la solicitud", + "oauth_timeout": "Tiempo de espera agotado para la solicitud", "oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos", "password_enable_description": "Iniciar sesión con correo electrónico y contraseña", "password_settings": "Contraseña de Acceso", @@ -384,8 +395,6 @@ "admin_password": "Contraseña del administrador", "administration": "Administración", "advanced": "Avanzada", - "advanced_settings_beta_timeline_subtitle": "Prueba la nueva experiencia de la aplicación", - "advanced_settings_beta_timeline_title": "Cronología beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opción para filtrar medios durante la sincronización según criterios alternativos. Intenta esto solo si tienes problemas con que la aplicación detecte todos los álbumes.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbumes del dispositivo", "advanced_settings_log_level_title": "Nivel de registro: {level}", @@ -393,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirá en cada petición de red", "advanced_settings_proxy_headers_title": "Cabeceras Proxy", + "advanced_settings_readonly_mode_subtitle": "Habilita el modo de solo lectura donde las fotografías sólo pueden ser vistas, funciones como seleccionar múltiples imágenes, compartir, transmitir, eliminar son deshabilitadas. Habilita/Deshabilita solo lectura vía el avatar del usuario en la pantalla principal", + "advanced_settings_readonly_mode_title": "Modo Solo lectura", "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados.", "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados", "advanced_settings_sync_remote_deletions_subtitle": "Eliminar o restaurar automáticamente un recurso en este dispositivo cuando se realice esa acción en la web", @@ -420,6 +431,7 @@ "album_remove_user_confirmation": "¿Estás seguro de que quieres eliminar a {user}?", "album_search_not_found": "No se encontraron álbumes que coincidan con tu búsqueda", "album_share_no_users": "Parece que has compartido este álbum con todos los usuarios o no tienes ningún usuario con quien compartirlo.", + "album_summary": "Resumen del álbum", "album_updated": "Album actualizado", "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos archivos", "album_user_left": "Salida {album}", @@ -458,6 +470,7 @@ "app_bar_signout_dialog_title": "Cerrar sesión", "app_settings": "Ajustes de la aplicacion", "appears_in": "Aparece en", + "apply_count": "Aplicar ({count, number})", "archive": "Archivo", "archive_action_prompt": "{count} agregado(s) al archivo", "archive_or_unarchive_photo": "Archivar o restaurar foto", @@ -490,6 +503,8 @@ "asset_restored_successfully": "Elementos restaurados exitosamente", "asset_skipped": "Omitido", "asset_skipped_in_trash": "En la papelera", + "asset_trashed": "Elemento eliminado", + "asset_troubleshoot": "Diagnóstico del elemento", "asset_uploaded": "Subido", "asset_uploading": "Subiendo…", "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos", @@ -497,7 +512,9 @@ "assets": "elementos", "assets_added_count": "{count, plural, one {# elemento agregado} other {# elementos agregados}}", "assets_added_to_album_count": "{count, plural, one {# elemento agregado} other {# elementos agregados}} al álbum", + "assets_added_to_albums_count": "{assetTotal, plural, one {# agregado} other {# agregados}} {albumTotal, plural, one {# al álbum} other {# a los álbumes}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {El elemento no se puede agregar al álbum} other {Los elementos no se pueden agregar al álbum}}", + "assets_cannot_be_added_to_albums": "{count, plural, one {El elemento} other {Los elementos}} no se {count, plural, one {puede} other {pueden}} agregar a ninguno de los álbumes", "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_deleted_permanently": "{count} elemento(s) eliminado(s) permanentemente", "assets_deleted_permanently_from_server": "{count} recurso(s) eliminado(s) de forma permanente del servidor de Immich", @@ -514,14 +531,17 @@ "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", "assets_trashed_from_server": "{count} recurso(s) enviado(s) a la papelera desde el servidor de Immich", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", + "assets_were_part_of_albums_count": "{count, plural, one {El elemento ya es} other {Los elementos ya son}} parte de los álbumes", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares", "automatic_endpoint_switching_title": "Cambio automático de URL", "autoplay_slideshow": "Presentación con reproducción automática", "back": "Atrás", "back_close_deselect": "Atrás, cerrar o anular la selección", + "background_backup_running_error": "Ya se está ejecutando la copia de seguridad en segundo plano, no se puede iniciar la copia de seguridad manual", "background_location_permission": "Permiso de ubicación en segundo plano", "background_location_permission_content": "Para poder cambiar de red mientras se ejecuta en segundo plano, Immich debe tener *siempre* acceso a la ubicación precisa para que la aplicación pueda leer el nombre de la red Wi-Fi", + "background_options": "Opciones de segundo plano", "backup": "Copia de Seguridad", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", @@ -529,6 +549,7 @@ "backup_album_selection_page_select_albums": "Seleccionar álbumes", "backup_album_selection_page_selection_info": "Información sobre la Selección", "backup_album_selection_page_total_assets": "Total de elementos únicos", + "backup_albums_sync": "Sincronización de álbumes de respaldo", "backup_all": "Todos", "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…", "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…", @@ -588,8 +609,6 @@ "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano", "backup_settings_subtitle": "Configura las opciones de subida", "backward": "Retroceder", - "beta_sync": "Estado de Sincronización Beta", - "beta_sync_subtitle": "Administrar el nuevo sistema de sincronización", "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", @@ -647,6 +666,8 @@ "change_pin_code": "Cambiar PIN", "change_your_password": "Cambia tu contraseña", "changed_visibility_successfully": "Visibilidad cambiada correctamente", + "charging": "Cargando", + "charging_requirement_mobile_backup": "La copia de seguridad en segundo plano requiere que el dispositivo se esté cargando", "check_corrupt_asset_backup": "Comprobar copias de seguridad de archivos corruptos", "check_corrupt_asset_backup_button": "Realizar comprobación", "check_corrupt_asset_backup_description": "Ejecutar esta comprobación solo por Wi-Fi y una vez que todos los archivos hayan sido respaldados. El procedimiento puede tardar unos minutos.", @@ -678,7 +699,7 @@ "comments_and_likes": "Comentarios y me gusta", "comments_are_disabled": "Los comentarios están deshabilitados", "common_create_new_album": "Crear nuevo álbum", - "common_server_error": "Por favor, verifica tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.", + "common_server_error": "Por favor, comprueba tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.", "completed": "Completado", "confirm": "Confirmar", "confirm_admin_password": "Confirmar contraseña del administrador", @@ -733,6 +754,7 @@ "create_user": "Crear usuario", "created": "Creado", "created_at": "Creado", + "creating_linked_albums": "Creando álbumes vinculados...", "crop": "Recortar", "curated_object_page_title": "Objetos", "current_device": "Dispositivo actual", @@ -791,7 +813,7 @@ "deletes_missing_assets": "Elimina archivos que faltan en el disco duro", "description": "Descripción", "description_input_hint_text": "Agregar descripción...", - "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", + "description_input_submit_error": "Error al actualizar la descripción, comprueba el registro para obtener más detalles", "deselect_all": "Deseleccionar Todo", "details": "Detalles", "direction": "Dirección", @@ -882,7 +904,9 @@ "error": "Error", "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_getting_places": "Error obteniendo lugares", "error_loading_image": "Error al cargar la imagen", + "error_loading_partners": "Error al cargar compañeros: {error}", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error al etiquetar la cara: no se pueden obtener las coordenadas del marco", "error_title": "Error: algo salió mal", @@ -1034,7 +1058,7 @@ "external": "Externo", "external_libraries": "Bibliotecas externas", "external_network": "Red externa", - "external_network_sheet_info": "Cuando no tengas conexión con tu red Wi-Fi preferida, la aplicación se conectará al servidor utilizando la primera de las URL siguientes a la que pueda acceder. Las URL se probarán de arriba hacia abajo.", + "external_network_sheet_info": "Cuando no tengas conexión con tu red Wi-Fi preferida, la aplicación se conectará al servidor utilizando la primera de las URL siguientes a la que pueda acceder, empezando de arriba hacia abajo", "face_unassigned": "Sin asignar", "failed": "Fallido", "failed_to_authenticate": "Fallo al autentificar", @@ -1047,6 +1071,7 @@ "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos", "feature_photo_updated": "Foto destacada actualizada", "features": "Características", + "features_in_development": "Funciones en Desarrollo", "features_setting_description": "Administrar las funciones de la aplicación", "file_name": "Nombre de archivo", "file_name_or_extension": "Nombre del archivo o extensión", @@ -1056,6 +1081,7 @@ "filter_people": "Filtrar personas", "filter_places": "Filtrar lugares", "find_them_fast": "Encuéntrelos rápidamente por nombre con la búsqueda", + "first": "Primero", "fix_incorrect_match": "Corregir coincidencia incorrecta", "folder": "Carpeta", "folder_not_found": "Carpeta no encontrada", @@ -1066,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidad carga recursos externos desde Google para poder funcionar.", "general": "General", + "geolocation_instruction_location": "Da click en un asset con coordenadas GPS para usar su ubicacion, o selecciona una ubicacion directamente en el mapa", "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", "go_back": "Volver atrás", "go_to_folder": "Ir al directorio", "go_to_search": "Ir a búsqueda", + "gps": "GPS", + "gps_missing": "Sin GPS", "grant_permission": "Conceder permiso", "group_albums_by": "Agrupar álbumes por...", "group_country": "Agrupar por país", @@ -1177,6 +1206,7 @@ "language_search_hint": "Buscar idiomas...", "language_setting_description": "Selecciona tu idioma preferido", "large_files": "Archivos Grandes", + "last": "Último", "last_seen": "Ultima vez visto", "latest_version": "Última versión", "latitude": "Latitud", @@ -1206,6 +1236,7 @@ "local": "Local", "local_asset_cast_failed": "No es posible transmitir un recurso que no está subido al servidor", "local_assets": "Archivos Locales", + "local_media_summary": "Resumen de Medios Locales", "local_network": "Red local", "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", @@ -1217,6 +1248,7 @@ "location_picker_longitude_hint": "Introduce tu longitud aquí", "lock": "Bloquear", "locked_folder": "Carpeta protegida", + "log_detail_title": "Detalle del registro", "log_out": "Cerrar sesión", "log_out_all_devices": "Cerrar sesión en todos los dispositivos", "logged_in_as": "Sesión iniciada como {user}", @@ -1224,7 +1256,7 @@ "logged_out_device": "Dispositivo desconectado", "login": "Inicio de sesión", "login_disabled": "El inicio de sesión ha sido desactivado", - "login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.", + "login_form_api_exception": "Excepción producida por API. Por favor, comprueba el URL del servidor e inténtalo de nuevo.", "login_form_back_button_text": "Atrás", "login_form_email_hint": "tucorreo@correo.com", "login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto", @@ -1234,7 +1266,7 @@ "login_form_err_invalid_url": "URL no válida", "login_form_err_leading_whitespace": "Espacio en blanco inicial", "login_form_err_trailing_whitespace": "Espacio en blanco al final", - "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, verifica la URL del servidor", + "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, comprueba la URL del servidor", "login_form_failed_get_oauth_server_disable": "La función de OAuth no está disponible en este servidor", "login_form_failed_login": "Error al iniciar sesión, comprueba la URL del servidor, el correo electrónico y la contraseña", "login_form_handshake_exception": "Hubo una excepción de handshake con el servidor. Activa la compatibilidad con certificados autofirmados en la configuración si estás utilizando un certificado autofirmado.", @@ -1247,6 +1279,7 @@ "login_password_changed_success": "Contraseña cambiado con éxito", "logout_all_device_confirmation": "¿Estás seguro de que quieres cerrar sesión en todos los dispositivos?", "logout_this_device_confirmation": "¿Estás seguro de que quieres cerrar sesión en este dispositivo?", + "logs": "Registros", "longitude": "Longitud", "look": "Mirar", "loop_videos": "Vídeos en bucle", @@ -1254,6 +1287,7 @@ "main_branch_warning": "Está utilizando una versión de desarrollo; ¡le recomendamos encarecidamente que utilice una versión de lanzamiento!", "main_menu": "Menú principal", "make": "Marca", + "manage_geolocation": "Administrar ubicación", "manage_shared_links": "Administrar enlaces compartidos", "manage_sharing_with_partners": "Gestionar el uso compartido con compañeros", "manage_the_app_settings": "Administrar la configuración de la aplicación", @@ -1288,6 +1322,7 @@ "mark_as_read": "Marcar como leído", "marked_all_as_read": "Todos marcados como leídos", "matches": "Coincidencias", + "matching_assets": "Elementos Coincidentes", "media_type": "Tipo de medio", "memories": "Recuerdos", "memories_all_caught_up": "Puesto al día", @@ -1328,6 +1363,7 @@ "name_or_nickname": "Nombre o apodo", "network_requirement_photos_upload": "Usar datos móviles para crear una copia de seguridad de las fotos", "network_requirement_videos_upload": "Usar datos móviles para crear una copia de seguridad de los videos", + "network_requirements": "Requisitos de red", "network_requirements_updated": "Los requisitos de red han cambiado, reiniciando la cola de copias de seguridad", "networking_settings": "Red", "networking_subtitle": "Configuraciones de acceso por URL al servidor", @@ -1337,7 +1373,8 @@ "new_password": "Nueva contraseña", "new_person": "Nueva persona", "new_pin_code": "Nuevo PIN", - "new_pin_code_subtitle": "Esta es la primera vez que accedes a la carpeta protegida. Crea un PIN seguro para acceder a esta página.", + "new_pin_code_subtitle": "Esta es la primera vez que accedes a la carpeta protegida. Crea un código PIN seguro para acceder a esta página", + "new_timeline": "Nueva Línea de tiempo", "new_user_created": "Nuevo usuario creado", "new_version_available": "NUEVA VERSIÓN DISPONIBLE", "newest_first": "El más reciente primero", @@ -1351,20 +1388,25 @@ "no_assets_message": "HAZ CLIC PARA SUBIR TU PRIMERA FOTO", "no_assets_to_show": "No hay elementos a mostrar", "no_cast_devices_found": "No se encontraron dispositivos de transmisión", + "no_checksum_local": "Suma de verificación no disponible. No se pueden obtener los elementos locales", + "no_checksum_remote": "Suma de verificación no disponible. No se puede obtener el elemento remoto", "no_duplicates_found": "No se encontraron duplicados.", "no_exif_info_available": "No hay información exif disponible", "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_local_assets_found": "No se encontraron elementos locales con esta suma de comprobación", "no_locked_photos_message": "Las fotos y los vídeos de la carpeta protegida se mantienen ocultos; no aparecerán cuando veas o busques elementos en tu biblioteca.", "no_name": "Sin nombre", "no_notifications": "Ninguna notificación", "no_people_found": "No se encontraron personas coincidentes", "no_places": "Sin lugares", + "no_remote_assets_found": "No se encontraron elementos remotos con esta suma de comprobación", "no_results": "Sin resultados", "no_results_description": "Pruebe con un sinónimo o una palabra clave más general", "no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red", "no_uploads_in_progress": "No hay cargas en progreso", + "not_available": "N/D", "not_in_any_album": "Sin álbum", "not_selected": "No seleccionado", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos que ya se subieron, ejecute la", @@ -1399,10 +1441,12 @@ "open_the_search_filters": "Abre los filtros de búsqueda", "options": "Opciones", "or": "o", + "organize_into_albums": "Organizar en álbumes", + "organize_into_albums_description": "Añade fotos existentes en álbumes usando la configuración actual de sincronización", "organize_your_library": "Organiza tu biblioteca", "original": "original", "other": "Otro", - "other_devices": "Otro dispositivo", + "other_devices": "Otros dispositivos", "other_entities": "Otras entidades", "other_variables": "Otras variables", "owned": "Propios", @@ -1484,6 +1528,7 @@ "port": "Puerto", "preferences_settings_subtitle": "Configuraciones de la aplicación", "preferences_settings_title": "Preferencias", + "preparing": "Preparando", "preset": "Preestablecido", "preview": "Posterior", "previous": "Anterior", @@ -1500,6 +1545,7 @@ "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": "Cliente y Servidor están actualizados", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Modo Solo lectura habilitado. Mantén pulsado el icono del avatar del usuario para salir.", "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}", @@ -1538,6 +1584,7 @@ "purchase_server_description_2": "Estado del soporte", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", + "query_asset_id": "Consultar ID de elemento", "queue_status": "Poniendo en cola {count}/{total}", "rating": "Valoración", "rating_clear": "Borrar calificación", @@ -1545,6 +1592,9 @@ "rating_description": "Mostrar la clasificación exif en el panel de información", "reaction_options": "Opciones de reacción", "read_changelog": "Leer registro de cambios", + "readonly_mode_disabled": "Modo Solo lectura deshabilitado", + "readonly_mode_enabled": "Modo Solo lectura habilitado", + "ready_for_upload": "Listo para subir", "reassign": "Reasignar", "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a {name, select, null {una persona existente} other {{name}}}", "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario", @@ -1569,6 +1619,7 @@ "regenerating_thumbnails": "Recargando miniaturas", "remote": "Remoto", "remote_assets": "Elementos remotos", + "remote_media_summary": "Resumen de Medios Remotos", "remove": "Eliminar", "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del álbum?", "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?", @@ -1582,8 +1633,8 @@ "remove_from_locked_folder": "Eliminar de la carpeta protegida", "remove_from_locked_folder_confirmation": "¿Seguro que deseas sacar estas fotos y vídeos de la carpeta protegida? Si continúas, los elementos 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", + "remove_memory": "Quitar recuerdo", + "remove_photo_from_memory": "Quitar foto de este recuerdo", "remove_tag": "Quitar etiqueta", "remove_url": "Eliminar URL", "remove_user": "Eliminar usuario", @@ -1591,8 +1642,8 @@ "removed_from_archive": "Eliminado del archivo", "removed_from_favorites": "Eliminado de favoritos", "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos", - "removed_memory": "Memoria eliminada", - "removed_photo_from_memory": "Se ha eliminado la foto de la memoria", + "removed_memory": "Recuerdo eliminado", + "removed_photo_from_memory": "Foto eliminada del recuerdo", "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", "rename": "Renombrar", "repair": "Reparar", @@ -1602,7 +1653,7 @@ "require_password": "Contraseña requerida", "require_user_to_change_password_on_first_login": "Requerir que el usuario cambie la contraseña en el primer inicio de sesión", "rescan": "Volver a escanear", - "reset": "Reiniciar", + "reset": "Restablecer", "reset_password": "Restablecer la contraseña", "reset_people_visibility": "Restablecer la visibilidad de las personas", "reset_pin_code": "Restablecer PIN", @@ -1621,6 +1672,7 @@ "restore_user": "Restaurar usuario", "restored_asset": "Archivo restaurado", "resume": "Continuar", + "resume_paused_jobs": "Reanudar {count, plural, one {# tarea en pausa} other {# tareas en pausa}}", "retry_upload": "Reintentar subida", "review_duplicates": "Revisar duplicados", "review_large_files": "Revisar archivos grandes", @@ -1714,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Fallo al crear el álbum", "selected": "Seleccionado", "selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}", + "selected_gps_coordinates": "Coordenadas GPS seleccionadas", "send_message": "Enviar mensaje", "send_welcome_email": "Enviar correo de bienvenida", "server_endpoint": "Punto final del servidor", @@ -1842,6 +1895,7 @@ "show_slideshow_transition": "Mostrar la transición de las diapositivas", "show_supporter_badge": "Insignia de colaborador", "show_supporter_badge_description": "Mostrar una insignia de colaborador", + "show_text_search_menu": "Mostrar el menú de búsqueda", "shuffle": "Modo aleatorio", "sidebar": "Barra lateral", "sidebar_display_description": "Muestra un enlace a la vista en la barra lateral", @@ -1862,7 +1916,7 @@ "sort_people_by_similarity": "Ordenar personas por similitud", "sort_recent": "Foto más reciente", "sort_title": "Título", - "source": "Origen", + "source": "Fuente", "stack": "Apilar", "stack_action_prompt": "{count} apilados", "stack_duplicates": "Apilar duplicados", @@ -1872,6 +1926,7 @@ "stacktrace": "Seguimiento de pila", "start": "Inicio", "start_date": "Fecha de inicio", + "start_date_before_end_date": "Fecha de inicio debe ser antes de fecha final", "state": "Estado", "status": "Estado", "stop_casting": "Detener transmisión", @@ -1896,6 +1951,8 @@ "sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los álbumes seleccionados a respaldar", "sync_local": "Sincronización Local", "sync_remote": "Sincronización Remota", + "sync_status": "Estado de la sincronización", + "sync_status_subtitle": "Ver y gestionar el estado de la sincronización", "sync_upload_album_setting_subtitle": "Crea y sube tus fotos y videos a los álbumes seleccionados en Immich", "tag": "Etiqueta", "tag_assets": "Etiquetar activos", @@ -1933,7 +1990,9 @@ "to_change_password": "Cambiar contraseña", "to_favorite": "A los favoritos", "to_login": "Iniciar Sesión", + "to_multi_select": "para multi selección", "to_parent": "Ir a los padres", + "to_select": "para seleccionar", "to_trash": "Descartar", "toggle_settings": "Alternar ajustes", "total": "Total", @@ -1953,6 +2012,7 @@ "trash_page_select_assets_btn": "Seleccionar elementos", "trash_page_title": "Papelera ({count})", "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serán eliminados permanentemente tras {days, plural, one {# día} other {# días}}.", + "troubleshoot": "Solucionar problemas", "type": "Tipo", "unable_to_change_pin_code": "No se ha podido cambiar el PIN", "unable_to_setup_pin_code": "No se ha podido establecer el PIN", @@ -1983,6 +2043,7 @@ "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", "untagged": "Sin etiqueta", "up_next": "A continuación", + "update_location_action_prompt": "Actualiza la ubicación de {count} assets seleccionados con:", "updated_at": "Actualizado", "updated_password": "Contraseña actualizada", "upload": "Subir", @@ -2042,13 +2103,14 @@ "view_all": "Ver todas", "view_all_users": "Mostrar todos los usuarios", "view_details": "Ver Detalles", - "view_in_timeline": "Mostrar en la línea de tiempo", + "view_in_timeline": "Ver en la línea de tiempo", "view_link": "Ver enlace", "view_links": "Mostrar enlaces", "view_name": "Ver", "view_next_asset": "Mostrar siguiente elemento", "view_previous_asset": "Mostrar elemento anterior", "view_qr_code": "Ver código QR", + "view_similar_photos": "Ver fotografías similares", "view_stack": "Ver Pila", "view_user": "Ver Usuario", "viewer_remove_from_stack": "Quitar de la pila", @@ -2067,5 +2129,6 @@ "yes": "Sí", "you_dont_have_any_shared_links": "No tienes ningún enlace compartido", "your_wifi_name": "El nombre de tu Wi-Fi", - "zoom_image": "Acercar Imagen" + "zoom_image": "Acercar Imagen", + "zoom_to_bounds": "Ajustar a los límites" } diff --git a/i18n/et.json b/i18n/et.json index 8206dbe37e..d451606dae 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -28,6 +28,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_album_bottom_sheet_some_local_assets": "Kõiki lokaalseid üksuseid ei õnnestunud albumisse lisada", "add_to_album_toggle": "Muuda albumi {album} valikut", "add_to_albums": "Lisa albumitesse", "add_to_albums_count": "Lisa albumitesse ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Luba logimine", "logging_level_description": "Kui lubatud, millist logimistaset kasutada.", "logging_settings": "Logimine", + "machine_learning_availability_checks": "Saadavuskontrollid", + "machine_learning_availability_checks_description": "Tuvasta ja eelista automaatselt saadavalolevaid masinõppeservereid", + "machine_learning_availability_checks_enabled": "Luba saadavuskontrollid", + "machine_learning_availability_checks_interval": "Kontrolli intervall", + "machine_learning_availability_checks_interval_description": "Saadavuskontrollide intervall millisekundites", + "machine_learning_availability_checks_timeout": "Päringu ajalõpp", + "machine_learning_availability_checks_timeout_description": "Saadavuskontrollide ajalõpp millisekundites", "machine_learning_clip_model": "CLIP mudel", "machine_learning_clip_model_description": "CLIP mudeli nimi, mis on loetletud siin. Pane tähele, et mudeli muutmisel pead kõigi piltide peal nutiotsingu tööte uuesti käivitama.", "machine_learning_duplicate_detection": "Duplikaatide leidmine", @@ -387,8 +395,6 @@ "admin_password": "Administraatori parool", "administration": "Administratsioon", "advanced": "Täpsemad valikud", - "advanced_settings_beta_timeline_subtitle": "Koge uut rakendust", - "advanced_settings_beta_timeline_title": "Beeta ajajoon", "advanced_settings_enable_alternate_media_filter_subtitle": "Kasuta seda valikut, et filtreerida sünkroonimise ajal üksuseid alternatiivsete kriteeriumite alusel. Proovi seda ainult siis, kui rakendusel on probleeme kõigi albumite tuvastamisega.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAALNE] Kasuta alternatiivset seadme albumi sünkroonimise filtrit", "advanced_settings_log_level_title": "Logimistase: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Eelista kaugpilte", "advanced_settings_proxy_headers_subtitle": "Määra vaheserveri päised, mida Immich peaks iga päringuga saatma", "advanced_settings_proxy_headers_title": "Vaheserveri päised", + "advanced_settings_readonly_mode_subtitle": "Lülitab sisse kirjutuskaitserežiimi, milles saab fotosid ainult vaadata ning toimingud nagu mitme pildi valimine, jagamine, edastamine ja kustutamine on keelatud. Lülita kirjutuskaitserežiim sisse/välja põhiekraanil oleva avatari kaudu", + "advanced_settings_readonly_mode_title": "Kirjutuskaitserežiim", "advanced_settings_self_signed_ssl_subtitle": "Jätab serveri lõpp-punkti SSL-sertifikaadi kontrolli vahele. Nõutud endasigneeritud sertifikaatide jaoks.", "advanced_settings_self_signed_ssl_title": "Luba endasigneeritud SSL-sertifikaadid", "advanced_settings_sync_remote_deletions_subtitle": "Kustuta või taasta üksus selles seadmes automaatself, kui sama tegevus toimub veebis", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?", "album_search_not_found": "Otsingule vastavaid albumeid ei leitud", "album_share_no_users": "Paistab, et oled seda albumit kõikide kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.", + "album_summary": "Albumi kokkuvõte", "album_updated": "Album muudetud", "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid", "album_user_left": "Lahkutud albumist {album}", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Logi välja", "app_settings": "Rakenduse seaded", "appears_in": "Albumid", + "apply_count": "Rakenda ({count, number})", "archive": "Arhiiv", "archive_action_prompt": "{count} lisatud arhiivi", "archive_or_unarchive_photo": "Arhiveeri või taasta foto", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Üksus edukalt taastatud", "asset_skipped": "Vahele jäetud", "asset_skipped_in_trash": "Prügikastis", + "asset_trashed": "Üksus liigutatud prügikasti", + "asset_troubleshoot": "Üksuse tõrkeotsing", "asset_uploaded": "Üleslaaditud", "asset_uploading": "Üleslaadimine…", "asset_viewer_settings_subtitle": "Halda galeriivaaturi seadeid", @@ -500,7 +512,7 @@ "assets": "Üksused", "assets_added_count": "{count, plural, one {# üksus} other {# üksust}} lisatud", "assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud", - "assets_added_to_albums_count": "{assetTotal, plural, one {# üksus} other {# üksust}} lisatud {albumTotal} albumisse", + "assets_added_to_albums_count": "{assetTotal, plural, one {# üksus} other {# üksust}} lisatud {albumTotal, plural, one {# albumisse} other {# albumisse}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Üksust} other {Üksuseid}} ei saa albumisse lisada", "assets_cannot_be_added_to_albums": "{count, plural, one {Üksust} other {Üksuseid}} ei saa lisada ühtegi albumisse", "assets_count": "{count, plural, one {# üksus} other {# üksust}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Esita slaidiesitlus automaatselt", "back": "Tagasi", "back_close_deselect": "Tagasi, sulge või tühista valik", + "background_backup_running_error": "Taustvarundus on käimas, ei saa käsitsi varundust alustada", "background_location_permission": "Taustal asukoha luba", "background_location_permission_content": "Et taustal töötades võrguühendust vahetada, peab Immich'il *alati* olema täpse asukoha luba, et rakendus saaks WiFi-võrgu nime lugeda", + "background_options": "Taustavalikud", "backup": "Varundamine", "backup_album_selection_page_albums_device": "Albumid seadmel ({count})", "backup_album_selection_page_albums_tap": "Puuduta kaasamiseks, topeltpuuduta välistamiseks", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Vali albumid", "backup_album_selection_page_selection_info": "Valiku info", "backup_album_selection_page_total_assets": "Unikaalseid üksuseid kokku", + "backup_albums_sync": "Varundusalbumite sünkroniseerimine", "backup_all": "Kõik", "backup_background_service_backup_failed_message": "Üksuste varundamine ebaõnnestus. Uuesti proovimine…", "backup_background_service_connection_failed_message": "Serveriga ühendumine ebaõnnestus. Uuesti proovimine…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "Halda taustal ja esiplaanil üleslaadimise seadeid", "backup_settings_subtitle": "Halda üleslaadimise seadeid", "backward": "Tagasi", - "beta_sync": "Beeta sünkroonimise staatus", - "beta_sync_subtitle": "Halda uut sünkroonimissüsteemi", "biometric_auth_enabled": "Biomeetriline autentimine lubatud", "biometric_locked_out": "Biomeetriline autentimine on blokeeritud", "biometric_no_options": "Biomeetrilisi valikuid ei ole", @@ -653,6 +666,8 @@ "change_pin_code": "Muuda PIN-koodi", "change_your_password": "Muuda oma parooli", "changed_visibility_successfully": "Nähtavus muudetud", + "charging": "Laadimine", + "charging_requirement_mobile_backup": "Taustal varundus vajab, et seade oleks laadimas", "check_corrupt_asset_backup": "Otsi riknenud üksuste varukoopiaid", "check_corrupt_asset_backup_button": "Teosta kontroll", "check_corrupt_asset_backup_description": "Käivita see kontroll ainult WiFi-võrgus ja siis, kui kõik üksused on varundatud. See protseduur võib kesta mõne minuti.", @@ -739,6 +754,7 @@ "create_user": "Lisa kasutaja", "created": "Lisatud", "created_at": "Lisatud", + "creating_linked_albums": "Lingitud albumite loomine...", "crop": "Kärpimine", "curated_object_page_title": "Asjad", "current_device": "Praegune seade", @@ -831,11 +847,11 @@ "download_settings_description": "Halda üksuste allalaadimise seadeid", "download_started": "Allalaadimine alustatud", "download_sucess": "Allalaadimine õnnestus", - "download_sucess_android": "Meediumid laaditi alla kataloogi DCIM/Immich", + "download_sucess_android": "Üksused laaditi alla kataloogi DCIM/Immich", "download_waiting_to_retry": "Uuesti proovimise ootel", "downloading": "Allalaadimine", "downloading_asset_filename": "Üksuse {filename} allalaadimine", - "downloading_media": "Meediumi allalaadimine", + "downloading_media": "Üksuste allalaadimine", "drop_files_to_upload": "Failide üleslaadimiseks sikuta need ükskõik kuhu", "duplicates": "Duplikaadid", "duplicates_description": "Lahenda iga grupp, valides duplikaadid, kui neid on", @@ -888,7 +904,9 @@ "error": "Viga", "error_change_sort_album": "Albumi sorteerimisjärjestuse muutmine ebaõnnestus", "error_delete_face": "Viga näo kustutamisel", + "error_getting_places": "Viga kohtade pärimisel", "error_loading_image": "Viga pildi laadimisel", + "error_loading_partners": "Viga partnerite laadimisel: {error}", "error_saving_image": "Viga: {error}", "error_tag_face_bounding_box": "Viga näo sildistamisel - ümbritseva kasti koordinaate ei õnnestunud leida", "error_title": "Viga - midagi läks valesti", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "Lemmikuid üksuseid ei leitud", "feature_photo_updated": "Esiletõstetud foto muudetud", "features": "Funktsioonid", + "features_in_development": "Arendusjärgus olevad funktsioonid", "features_setting_description": "Halda rakenduse funktsioone", "file_name": "Failinimi", "file_name_or_extension": "Failinimi või -laiend", @@ -1073,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "See funktsionaalsus laadib töötamiseks Google'st väliseid ressursse.", "general": "Üldine", + "geolocation_instruction_location": "Klõpsa GPS-koordinaatidega üksusel, et kasutada selle asukohta, või vali asukoht otse kaardilt", "get_help": "Küsi abi", "get_wifiname_error": "WiFi-võrgu nime ei õnnestunud lugeda. Veendu, et oled andnud vajalikud load ja oled WiFi-võrguga ühendatud", "getting_started": "Alustamine", "go_back": "Tagasi", "go_to_folder": "Mine kausta", "go_to_search": "Otsingusse", + "gps": "GPS", + "gps_missing": "GPS puudub", "grant_permission": "Anna luba", "group_albums_by": "Grupeeri albumid...", "group_country": "Grupeeri riigi kaupa", @@ -1214,6 +1236,7 @@ "local": "Lokaalsed", "local_asset_cast_failed": "Ei saa edastada üksust, mis pole serverisse üles laaditud", "local_assets": "Lokaalsed üksused", + "local_media_summary": "Lokaalsete üksuste kokkuvõte", "local_network": "Kohalik võrk", "local_network_sheet_info": "Rakendus ühendub valitud Wi-Fi võrgus olles serveriga selle URL-i kaudu", "location_permission": "Asukoha luba", @@ -1225,6 +1248,7 @@ "location_picker_longitude_hint": "Sisesta pikkuskraad siia", "lock": "Lukusta", "locked_folder": "Lukustatud kaust", + "log_detail_title": "Logi detailid", "log_out": "Logi välja", "log_out_all_devices": "Logi kõigist seadmetest välja", "logged_in_as": "Logitud sisse kasutajana {user}", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "Parool edukalt uuendatud", "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", + "logs": "Logid", "longitude": "Pikkuskraad", "look": "Välimus", "loop_videos": "Taasesita videod", @@ -1262,6 +1287,7 @@ "main_branch_warning": "Sa kasutad arendusversiooni; soovitame tungivalt kasutada väljalaskeversiooni!", "main_menu": "Peamenüü", "make": "Mark", + "manage_geolocation": "Halda asukohta", "manage_shared_links": "Halda jagatud linke", "manage_sharing_with_partners": "Halda partneritega jagamist", "manage_the_app_settings": "Halda rakenduse seadeid", @@ -1296,6 +1322,7 @@ "mark_as_read": "Märgi loetuks", "marked_all_as_read": "Kõik märgiti loetuks", "matches": "Ühtivad failid", + "matching_assets": "Ühtivad üksused", "media_type": "Meediumi tüüp", "memories": "Mälestused", "memories_all_caught_up": "Ongi kõik", @@ -1336,6 +1363,7 @@ "name_or_nickname": "Nimi või hüüdnimi", "network_requirement_photos_upload": "Kasuta fotode varundamiseks mobiilset andmesidet", "network_requirement_videos_upload": "Kasuta videote varundamiseks mobiilset andmesidet", + "network_requirements": "Võrgu nõuded", "network_requirements_updated": "Võrgu nõuded muutusid, varundamise järjekord lähtestatakse", "networking_settings": "Võrguühendus", "networking_subtitle": "Halda serveri lõpp-punkti seadeid", @@ -1346,6 +1374,7 @@ "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_timeline": "Uus ajajoon", "new_user_created": "Uus kasutaja lisatud", "new_version_available": "UUS VERSIOON SAADAVAL", "newest_first": "Uuemad eespool", @@ -1359,16 +1388,20 @@ "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS", "no_assets_to_show": "Pole üksuseid, mida kuvada", "no_cast_devices_found": "Edastamise seadmeid ei leitud", + "no_checksum_local": "Kontrollsumma pole saadaval - lokaalse üksuse pärimine ebaõnnestus", + "no_checksum_remote": "Kontrollsumma pole saadaval - kaugüksuse pärimine ebaõnnestus", "no_duplicates_found": "Ühtegi duplikaati ei leitud.", "no_exif_info_available": "Exif info pole saadaval", "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_local_assets_found": "Selle kontrollsummaga lokaalseid üksuseid ei leitud", "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", "no_places": "Kohti ei ole", + "no_remote_assets_found": "Selle kontrollsummaga kaugüksuseid ei leitud", "no_results": "Vasteid pole", "no_results_description": "Proovi sünonüümi või üldisemat märksõna", "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada", @@ -1407,6 +1440,8 @@ "open_the_search_filters": "Ava otsingufiltrid", "options": "Valikud", "or": "või", + "organize_into_albums": "Organiseeri albumitesse", + "organize_into_albums_description": "Pane olemasolevad fotod albumitesse, kasutades jooksvaid sünkroonimise seadeid", "organize_your_library": "Korrasta oma kogu", "original": "originaal", "other": "Muud", @@ -1492,6 +1527,7 @@ "port": "Port", "preferences_settings_subtitle": "Halda rakenduse eelistusi", "preferences_settings_title": "Eelistused", + "preparing": "Ettevalmistamine", "preset": "Eelseadistus", "preview": "Eelvaade", "previous": "Eelmine", @@ -1508,6 +1544,7 @@ "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_readonly_mode": "Kirjutuskaitserežiim sisse lülitatud. Väljumiseks puuduta pikalt avatari ikooni.", "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", @@ -1553,6 +1590,9 @@ "rating_description": "Kuva infopaneelis EXIF hinnangut", "reaction_options": "Reaktsiooni valikud", "read_changelog": "Vaata muudatuste ülevaadet", + "readonly_mode_disabled": "Kirjutuskaitserežiim välja lülitatud", + "readonly_mode_enabled": "Kirjutuskaitserežiim sisse lülitatud", + "ready_for_upload": "Valmis üleslaadimiseks", "reassign": "Määra uuesti", "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", @@ -1577,6 +1617,7 @@ "regenerating_thumbnails": "Pisipiltide uuesti genereerimine", "remote": "Serveris", "remote_assets": "Kaugüksused", + "remote_media_summary": "Kaugüksuste kokkuvõte", "remove": "Eemalda", "remove_assets_album_confirmation": "Kas oled kindel, et soovid {count, plural, one {# üksuse} other {# üksust}} albumist eemaldada?", "remove_assets_shared_link_confirmation": "Kas oled kindel, et soovid eemaldada {count, plural, one {# üksuse} other {# üksust}} sellelt jagatud lingilt?", @@ -1629,6 +1670,7 @@ "restore_user": "Taasta kasutaja", "restored_asset": "Üksus taastatud", "resume": "Jätka", + "resume_paused_jobs": "Jätka {count, plural, one {# peatatud tööde} other {# peatatud töödet}}", "retry_upload": "Proovi üleslaadimist uuesti", "review_duplicates": "Vaata duplikaadid läbi", "review_large_files": "Vaata suured failid läbi", @@ -1722,6 +1764,7 @@ "select_user_for_sharing_page_err_album": "Albumi lisamine ebaõnnestus", "selected": "Valitud", "selected_count": "{count, plural, other {# valitud}}", + "selected_gps_coordinates": "Valitud GPS-koordinaadid", "send_message": "Saada sõnum", "send_welcome_email": "Saada tervituskiri", "server_endpoint": "Serveri lõpp-punkt", @@ -1850,6 +1893,7 @@ "show_slideshow_transition": "Kuva slaidiesitluse üleminekud", "show_supporter_badge": "Toetaja märk", "show_supporter_badge_description": "Kuva toetaja märki", + "show_text_search_menu": "Kuva tekstiotsingu menüüd", "shuffle": "Juhuslik", "sidebar": "Külgmenüü", "sidebar_display_description": "Kuva külgmenüüs linki vaatele", @@ -1880,6 +1924,7 @@ "stacktrace": "Pinujälg", "start": "Alusta", "start_date": "Alguskuupäev", + "start_date_before_end_date": "Alguskuupäev peab olema varasem kui lõppkuupäev", "state": "Osariik", "status": "Staatus", "stop_casting": "Lõpeta edastamine", @@ -1904,6 +1949,8 @@ "sync_albums_manual_subtitle": "Sünkrooni kõik üleslaaditud videod ja fotod valitud varundusalbumitesse", "sync_local": "Sünkrooni lokaalsed üksused", "sync_remote": "Sünkrooni kaugüksused", + "sync_status": "Sünkroonimise staatus", + "sync_status_subtitle": "Vaata ja halda sünkroonimissüsteemi", "sync_upload_album_setting_subtitle": "Loo ja laadi oma pildid ja videod üles Immich'isse valitud albumitesse", "tag": "Silt", "tag_assets": "Sildista üksuseid", @@ -1941,7 +1988,9 @@ "to_change_password": "Muuda parool", "to_favorite": "Lemmik", "to_login": "Logi sisse", + "to_multi_select": "vali mitu", "to_parent": "Tase üles", + "to_select": "vali", "to_trash": "Prügikasti", "toggle_settings": "Kuva/peida seaded", "total": "Kokku", @@ -1961,6 +2010,7 @@ "trash_page_select_assets_btn": "Vali üksused", "trash_page_title": "Prügikast ({count})", "trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.", + "troubleshoot": "Tõrkeotsing", "type": "Tüüp", "unable_to_change_pin_code": "PIN-koodi muutmine ebaõnnestus", "unable_to_setup_pin_code": "PIN-koodi seadistamine ebaõnnestus", @@ -1991,6 +2041,7 @@ "unstacked_assets_count": "{count, plural, one {# üksus} other {# üksust}} eraldatud", "untagged": "Sildistamata", "up_next": "Järgmine", + "update_location_action_prompt": "Uuenda {count} valitud üksuse asukoht:", "updated_at": "Uuendatud", "updated_password": "Parool muudetud", "upload": "Laadi üles", @@ -2009,7 +2060,7 @@ "upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.", "upload_to_immich": "Laadi Immich'isse ({count})", "uploading": "Üleslaadimine", - "uploading_media": "Meediumi üleslaadimine", + "uploading_media": "Üksuste üleslaadimine", "url": "URL", "usage": "Kasutus", "use_biometric": "Kasuta biomeetriat", @@ -2057,6 +2108,7 @@ "view_next_asset": "Vaata järgmist üksust", "view_previous_asset": "Vaata eelmist üksust", "view_qr_code": "Vaata QR-koodi", + "view_similar_photos": "Vaata sarnaseid fotosid", "view_stack": "Vaata virna", "view_user": "Vaata kasutajat", "viewer_remove_from_stack": "Eemalda virnast", @@ -2075,5 +2127,6 @@ "yes": "Jah", "you_dont_have_any_shared_links": "Sul pole ühtegi jagatud linki", "your_wifi_name": "Sinu WiFi-võrgu nimi", - "zoom_image": "Suumi pilti" + "zoom_image": "Suumi pilti", + "zoom_to_bounds": "Suumi piiridesse" } diff --git a/i18n/eu.json b/i18n/eu.json index 311619ad90..bb16f126d6 100644 --- a/i18n/eu.json +++ b/i18n/eu.json @@ -38,6 +38,68 @@ "admin": { "add_exclusion_pattern_description": "Gehitu baztertze patroiak. *, ** eta ? karakterak erabil ditzazkezu (globbing). Adibideak: \"Raw\" izeneko edozein direktorioko fitxategi guztiak baztertzeko, erabili \"**/Raw/**\". \".tif\" amaitzen diren fitxategi guztiak baztertzeko, erabili \"**/*.tif\". Bide absolutu bat baztertzeko, erabili \"/baztertu/beharreko/bidea/**\".", "admin_user": "Administradore erabiltzailea", - "image_quality": "Kalitatea" - } + "authentication_settings": "Segurtasun Ezarpenak", + "authentication_settings_description": "Kudeatu pasahitza, OAuth edo beste segurtasun konfigurazio bat", + "authentication_settings_disable_all": "Seguru zaude saioa hasteko modu guztiak desgaitu nahi dituzula? Saioa hastea guztiz desgaitua izango da.", + "authentication_settings_reenable": "Berriro gaitzeko, erabili Server Command.", + "background_task_job": "Atzealdeko Lanak", + "backup_onboarding_footer": "Immich-en babes kopiei buruzko informazio gehiago nahi baduzu, mesedez irakurri dokumentazioa.", + "backup_onboarding_title": "Babes Kopiak", + "confirm_delete_library": "Seguru zaude {library} ezabatu nahi duzula?", + "confirm_email_below": "Konfirmatzeko, idatzi \"{email}\" azpian", + "confirm_reprocess_all_faces": "Seguru zaude aurpegi guztiak berriro prozesatu nahi dituzula? Erabakiak jendearen izenak ere borratuko ditu.", + "confirm_user_password_reset": "Seguru zaude {user}-ren pasahitza berrezarri nahi duzula?", + "confirm_user_pin_code_reset": "Seguru zaude {user}-ren PIN kodea berrezarri nahi duzula?", + "create_job": "Gehitu zeregina", + "disable_login": "Desgaitu saio hastea", + "face_detection": "Aurpegi detekzioa", + "failed_job_command": "{command} komandoak hutsegin du {job} zereginerako", + "image_format": "Formatua", + "image_format_description": "WebP ereduak JPEG baino fitxategi txikiagoak sortzen ditu, baina motelagoa da kodifikatzen.", + "image_preview_title": "Aurreikusiaen Konfigurazioa", + "image_quality": "Kalitatea", + "image_settings": "Argazkien Konfigurazioa", + "image_thumbnail_title": "Argazki Txikien Konfigurazioa", + "job_created": "Zeregina sortuta", + "job_settings": "Zereginaren konfigurazioa", + "job_status": "Zereginaren Egoera", + "logging_enable_description": "Gaitu erregistroak", + "logging_level_description": "Erregistroak gaituta daudenean, nolako erregistro maila erabili.", + "logging_settings": "Erregistroak", + "machine_learning_duplicate_detection": "Bizkoizketa Detekzioa", + "machine_learning_duplicate_detection_enabled": "Gaitu bikoizketa detekezioa", + "machine_learning_facial_recognition": "Aurpegi-Ezagutza", + "machine_learning_facial_recognition_description": "Detektatu, ezagutu eta aurpegiak banatu argazkietan", + "machine_learning_facial_recognition_model": "Aurpegi-Ezagutza eredua", + "machine_learning_facial_recognition_setting": "Aurpegi-Ezagutza Gaitu", + "machine_learning_smart_search_enabled": "Gaitu bilaketa arina", + "manage_log_settings": "Kudeatu erregistroen konfigurazioa", + "map_dark_style": "Beltz estiloa", + "map_gps_settings": "Mapa eta GPS Konfigurazioa", + "map_light_style": "Zuri estiloa", + "map_settings": "Mapa", + "metadata_faces_import_setting": "Gaitu aurpegien inportazioa", + "metadata_settings": "Metadata Konfigurazioa", + "metadata_settings_description": "Kudeatu metadaten konfigurazioa", + "migration_job": "Migrazio" + }, + "advanced_settings_readonly_mode_title": "Irakurri-bakarrik Modua", + "apply_count": "Ezarri ({count, number})", + "assets_added_to_albums_count": "Gehituta {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} ezin izan da albumetara gehitu", + "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} dagoeneko albumean dago", + "first": "Lehenengo «Lehenik»", + "gps": "GPS", + "gps_missing": "Ez dago GPS", + "last": "Azkena", + "like": "Gustoko", + "manage_geolocation": "Kudeatu kokapena", + "organize_into_albums": "Albumetan antolatu", + "query_asset_id": "Aztertu aukeratutako ID-a", + "readonly_mode_disabled": "Irakurri-bakarrik modua desgaituta", + "readonly_mode_enabled": "Irakurri-bakarrik modua gaituta", + "selected_gps_coordinates": "GPS Koordenadak Aukeratuta", + "sort_newest": "Argazkirik berriena", + "to_select": "aukeratzeko", + "view_similar_photos": "Ikusi antzeko argazkiak" } diff --git a/i18n/fa.json b/i18n/fa.json index 3b0be9a9b1..76f8d956fc 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -13,6 +13,7 @@ "add_a_location": "افزودن یک مکان", "add_a_name": "افزودن نام", "add_a_title": "افزودن عنوان", + "add_birthday": "افزودن تاریخ تولد", "add_exclusion_pattern": "افزودن الگوی استثنا", "add_import_path": "افزودن مسیر ورودی", "add_location": "افزودن مکان", @@ -22,10 +23,13 @@ "add_photos": "افزودن عکس ها", "add_to": "افزودن به …", "add_to_album": "افزودن به آلبوم", + "add_to_album_bottom_sheet_added": "به آلبوم {album} اضافه شد", + "add_to_album_bottom_sheet_already_exists": "قبلا در آلبوم {album} موجود است", + "add_to_album_bottom_sheet_some_local_assets": "برخی از محتواهای محلی را نشد به آلبوم اضافه کرد", "add_to_shared_album": "افزودن به آلبوم اشتراکی", "added_to_archive": "به آرشیو اضافه شد", "added_to_favorites": "به علاقه مندی ها اضافه شد", - "added_to_favorites_count": "{count} تا اضافه شد به علاقه مندی ها", + "added_to_favorites_count": "{count, number} تا به علاقه مندی ها اضافه شد", "admin": { "add_exclusion_pattern_description": "الگوهای استثنا را اضافه کنید. پشتیبانی از گلابینگ با استفاده از *, ** و ? وجود دارد. برای نادیده گرفتن تمام فایل‌ها در هر دایرکتوری با نام \"Raw\"، از \"**/Raw/**\" استفاده کنید. برای نادیده گرفتن تمام فایل‌هایی که با \".tif\" پایان می‌یابند، از \"**/*.tif\" استفاده کنید. برای نادیده گرفتن یک مسیر مطلق، از \"/path/to/ignore/**\" استفاده کنید.", "authentication_settings": "تنظیمات احراز هویت", diff --git a/i18n/fi.json b/i18n/fi.json index e62cd4ddc7..769b528f4c 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -28,6 +28,7 @@ "add_to_album": "Lisää albumiin", "add_to_album_bottom_sheet_added": "Lisätty albumiin {album}", "add_to_album_bottom_sheet_already_exists": "Kohde on jo albumissa {album}", + "add_to_album_toggle": "Vaihda albumin {album} valintaa", "add_to_albums": "Lisää albumeihin", "add_to_albums_count": "Lisää albumeihin ({count})", "add_to_shared_album": "Lisää jaettuun albumiin", @@ -122,6 +123,13 @@ "logging_enable_description": "Ota lokikirjaus käyttöön", "logging_level_description": "Kun käytössä, mitä lokituksen tasoa käytetään.", "logging_settings": "Lokit", + "machine_learning_availability_checks": "Saatavuustarkastukset", + "machine_learning_availability_checks_description": "Automaattisesti havaitse ja suosi vapaita koneoppimisen palvelimia", + "machine_learning_availability_checks_enabled": "Laita päälle saatavuus tarkistukset", + "machine_learning_availability_checks_interval": "Tarkastusväli", + "machine_learning_availability_checks_interval_description": "Aikaväli millisekunneissa saavutettavuus tarkistuksille", + "machine_learning_availability_checks_timeout": "Pyynnön aikakatkaisu", + "machine_learning_availability_checks_timeout_description": "Aikakatkaisu aika millisekunneissa saatavuus tarkistuksille", "machine_learning_clip_model": "CLIP-malli", "machine_learning_clip_model_description": "Käytettävän CLIP-mallin nimi toimivien mallien listasta. Huomaa että sinun täytyy suorittaa \"Älykäs etsintä\"-työ uudelleen vaihdettuasi käytettävää mallia.", "machine_learning_duplicate_detection": "Kaksoiskappaleiden tunnistus", @@ -386,8 +394,6 @@ "admin_password": "Ylläpitäjän salasana", "administration": "Ylläpito", "advanced": "Edistyneet", - "advanced_settings_beta_timeline_subtitle": "Kokeile uutta sovelluskokemusta", - "advanced_settings_beta_timeline_title": "Beta-aikajana", "advanced_settings_enable_alternate_media_filter_subtitle": "Käytä tätä vaihtoehtoa suodattaaksesi mediaa synkronoinnin aikana vaihtoehtoisten kriteerien perusteella. Kokeile tätä vain, jos sovelluksessa on ongelmia kaikkien albumien tunnistamisessa.", "advanced_settings_enable_alternate_media_filter_title": "[KOKEELLINEN] Käytä vaihtoehtoisen laitteen albumin synkronointisuodatinta", "advanced_settings_log_level_title": "Kirjaustaso: {level}", @@ -395,6 +401,8 @@ "advanced_settings_prefer_remote_title": "Suosi etäkuvia", "advanced_settings_proxy_headers_subtitle": "Määritä välityspalvelimen otsikot(proxy headers), jotka Immichin tulisi lähettää jokaisen verkkopyynnön mukana", "advanced_settings_proxy_headers_title": "Välityspalvelimen otsikot", + "advanced_settings_readonly_mode_subtitle": "Aktivoi vain luku -tilan, jolloin valokuvia voi ainoastaan selata. Toiminnot kuten useiden kuvien valitseminen, jakaminen, siirtäminen toistolaitteelle ja poistaminen ovat pois käytöstä. Laita vain luku -tila päälle tai pois päältä päävalikon käyttäjäkuvakkeesta", + "advanced_settings_readonly_mode_title": "Vain luku -tila", "advanced_settings_self_signed_ssl_subtitle": "Ohita SSL sertifikaattivarmennus palvelimen päätepisteellä. Vaaditaan self-signed -sertifikaateissa.", "advanced_settings_self_signed_ssl_title": "Salli self-signed SSL -sertifikaatit", "advanced_settings_sync_remote_deletions_subtitle": "Poista tai palauta kohde automaattisesti tällä laitteella, kun kyseinen toiminto suoritetaan verkossa", @@ -422,6 +430,7 @@ "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_search_not_found": "Haullasi ei löytynyt yhtään albumia", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", + "album_summary": "Albumi tiivistelmä", "album_updated": "Albumi päivitetty", "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä", "album_user_left": "Poistuttiin albumista {album}", @@ -460,6 +469,7 @@ "app_bar_signout_dialog_title": "Kirjaudu ulos", "app_settings": "Sovellusasetukset", "appears_in": "Esiintyy albumeissa", + "apply_count": "Aseta {count, number}", "archive": "Arkisto", "archive_action_prompt": "{count} lisätty arkistoon", "archive_or_unarchive_photo": "Arkistoi kuva tai palauta arkistosta", @@ -492,6 +502,7 @@ "asset_restored_successfully": "Kohde palautettu onnistuneesti", "asset_skipped": "Ohitettu", "asset_skipped_in_trash": "Roskakorissa", + "asset_trashed": "Kohde poistettu", "asset_uploaded": "Lähetetty", "asset_uploading": "Ladataan…", "asset_viewer_settings_subtitle": "Galleriakatseluohjelman asetusten hallinta", @@ -499,7 +510,9 @@ "assets": "Kohteet", "assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}", + "assets_added_to_albums_count": "Lisätty {assetTotal, plural, one {# aineisto} other {# aaineistoa}} {albumTotal, plural, one {# albumiin} other {# albumeihin}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Kohdetta} other {Kohdetta}} ei voida lisätä albumiin", + "assets_cannot_be_added_to_albums": "{count, plural, one {Aineisto} other {Aineistoa}} ei voi lisätä mihinkään albumiin", "assets_count": "{count, plural, one {# media} other {# mediaa}}", "assets_deleted_permanently": "{count} kohdetta poistettu pysyvästi", "assets_deleted_permanently_from_server": "{count} objektia poistettu pysyvästi Immich-palvelimelta", @@ -516,14 +529,17 @@ "assets_trashed_count": "{count, plural, one {# media} other {# mediaa}} siirretty roskakoriin", "assets_trashed_from_server": "{count} kohdetta siirretty roskakoriin Immich-palvelimelta", "assets_were_part_of_album_count": "{count, plural, one {Media oli} other {Mediat olivat}} jo albumissa", + "assets_were_part_of_albums_count": "{count, plural, one {Aineisto} other {Aineistoa}} oli jo albumeissa", "authorized_devices": "Valtuutetut laitteet", "automatic_endpoint_switching_subtitle": "Yhdistä paikallisesti nimetyn Wi-Fi-yhteyden kautta, kun se on saatavilla, ja käytä vaihtoehtoisia yhteyksiä muualla", "automatic_endpoint_switching_title": "Automaattinen URL-osoitteen vaihto", "autoplay_slideshow": "Toista diaesitys automaattisesti", "back": "Takaisin", "back_close_deselect": "Palaa, sulje tai poista valinnat", + "background_backup_running_error": "Tausta varmuuskopiointi on aktiivinen, ei voida aloittaa manuaalista varmuuskopiointia", "background_location_permission": "Taustasijainnin käyttöoikeus", "background_location_permission_content": "Jotta sovellus voi vaihtaa verkkoa taustalla toimiessaan, Immichillä on *aina* oltava pääsy tarkkaan sijaintiin, jotta se voi lukea Wi-Fi-verkon nimen", + "background_options": "Tausta valinnat", "backup": "Varmuuskopiointi", "backup_album_selection_page_albums_device": "Laitteen albumit ({count})", "backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois", @@ -590,8 +606,6 @@ "backup_setting_subtitle": "Hallinnoi aktiivisia ja taustalla olevia lähetysasetuksia", "backup_settings_subtitle": "Hallitse lähetysasetuksia", "backward": "Taaksepäin", - "beta_sync": "Betasynkronoinnin tila", - "beta_sync_subtitle": "Hallitse uutta synkronointijärjestelmää", "biometric_auth_enabled": "Biometrinen tunnistautuminen käytössä", "biometric_locked_out": "Sinulta on evätty pääsy biometriseen tunnistautumiseen", "biometric_no_options": "Ei biometrisiä vaihtoehtoja", @@ -763,7 +777,7 @@ "default_locale": "Oletuskieliasetus", "default_locale_description": "Muotoile päivämäärät ja numerot selaimesi kielen mukaan", "delete": "Poista", - "delete_action_confirmation_message": "Haluatko varmasti poistaa tämän aineiston? Tämä toiminto siirtää aineiston palvelimen roskakoriin ja kysyy, haluatko poistaa sen myös paikallisesti.", + "delete_action_confirmation_message": "Haluatko varmasti poistaa tämän aineiston? Tämä toiminto siirtää aineiston palvelimen roskakoriin ja kysyy, haluatko poistaa sen myös paikallisesti", "delete_action_prompt": "{count} poistettu", "delete_album": "Poista albumi", "delete_api_key_prompt": "Haluatko varmasti poistaa tämän API-avaimen?", @@ -862,7 +876,7 @@ "edit_title": "Muokkaa otsikkoa", "edit_user": "Muokkaa käyttäjää", "edited": "Muokattu", - "editor": "Editori", + "editor": "Muokkaaja", "editor_close_without_save_prompt": "Muutoksia ei tallenneta", "editor_close_without_save_title": "Suljetaanko editori?", "editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet", @@ -1058,6 +1072,7 @@ "filter_people": "Suodata henkilöt", "filter_places": "Suodata paikkoja", "find_them_fast": "Löydä nopeasti hakemalla nimellä", + "first": "Ensimmäinen", "fix_incorrect_match": "Korjaa virheellinen osuma", "folder": "Kansio", "folder_not_found": "Kansiota ei löytynyt", @@ -1068,6 +1083,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ominaisuus lataa ulkoisia resursseja Googlelta toimiakseen.", "general": "Yleinen", + "geolocation_instruction_location": "Napsauta kuvaa, jossa on GPS-koordinaatit, käyttääksesi sen sijaintia, tai valitse sijainti suoraan kartalta", "get_help": "Hae apua", "get_wifiname_error": "Wi-Fi-verkon nimen hakeminen epäonnistui. Varmista, että olet myöntänyt tarvittavat käyttöoikeudet ja että olet yhteydessä Wi-Fi-verkkoon", "getting_started": "Aloittaminen", @@ -1179,6 +1195,7 @@ "language_search_hint": "Etsi kieliä...", "language_setting_description": "Valitse suosimasi kieli", "large_files": "Suuret tiedostot", + "last": "Viimeinen", "last_seen": "Viimeksi nähty", "latest_version": "Viimeisin versio", "latitude": "Leveysaste", @@ -1197,6 +1214,7 @@ "library_page_sort_title": "Albumin otsikko", "licenses": "Lisenssit", "light": "Vaalea", + "like": "Tykkää", "like_deleted": "Tykkäys poistettu", "link_motion_video": "Linkitä liikevideo", "link_to_oauth": "Linkki OAuth", @@ -1459,9 +1477,9 @@ "permission_onboarding_permission_limited": "Rajoitettu käyttöoikeus. Salliaksesi Immichin varmuuskopioida ja hallita koko kuvakirjastoasi, myönnä oikeus kuviin ja videoihin asetuksista.", "permission_onboarding_request": "Immich vaatii käyttöoikeuden kuvien ja videoiden käyttämiseen.", "person": "Henkilö", - "person_age_months": "{months} kuukautta vanha", - "person_age_year_months": "1 vuosi ja {months} kuukautta vanha", - "person_age_years": "{years} vuotta vanha", + "person_age_months": "{months, plural, one {# kuukauden} other {# kuukauden}} ikäinen", + "person_age_year_months": "1 vuosi ja {months, plural, one {# kuukauden} other {# kuukautta}} vanha", + "person_age_years": "{years, plural, other {# vuotta}} vanha", "person_birthdate": "Syntynyt {date}", "person_hidden": "{name}{hidden, select, true { (piilotettu)} other {}}", "photo_shared_all_users": "Näyttää että olet jakanut kuvasi kaikkien käyttäjien kanssa, tai sinulla ei ole käyttäjää kenelle jakaa.", @@ -1626,8 +1644,8 @@ "review_duplicates": "Tarkastele kaksoiskappaleita", "review_large_files": "Tarkista suuret tiedostot", "role": "Rooli", - "role_editor": "Editori", - "role_viewer": "Toistin", + "role_editor": "Muokkaaja", + "role_viewer": "Katsoja", "running": "Käynnissä", "save": "Tallenna", "save_to_gallery": "Tallenna galleriaan", @@ -1858,6 +1876,7 @@ "sort_created": "Luontipäivä", "sort_items": "Tietueiden määrä", "sort_modified": "Muokkauspäivä", + "sort_newest": "Uusin kuva", "sort_oldest": "Vanhin kuva", "sort_people_by_similarity": "Lajittele ihmiset samankaltaisuuden mukaan", "sort_recent": "Tuorein kuva", diff --git a/i18n/fr.json b/i18n/fr.json index 53c68bcd39..d5b6e10ba8 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -28,6 +28,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_album_bottom_sheet_some_local_assets": "Certains médias n'ont pas pu être ajoutés à l'album", "add_to_album_toggle": "Basculer la sélection pour {album}", "add_to_albums": "Ajouter aux albums", "add_to_albums_count": "Ajouter aux albums ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Activer la journalisation", "logging_level_description": "Niveau de journalisation lorsque cette option est activée.", "logging_settings": "Journalisation", + "machine_learning_availability_checks": "Vérifications de disponibilité", + "machine_learning_availability_checks_description": "Détecte automatiquement et choisit les serveurs d'apprentissage machine disponibles", + "machine_learning_availability_checks_enabled": "Activer les vérifications de disponibilité", + "machine_learning_availability_checks_interval": "Intervalle de vérification", + "machine_learning_availability_checks_interval_description": "Intervalle en millisecondes entre les vérifications de disponibilité", + "machine_learning_availability_checks_timeout": "Délai d'expiration de la requête", + "machine_learning_availability_checks_timeout_description": "Délai d'expiration en millisecondes pour les vérifications de disponibilité", "machine_learning_clip_model": "Modèle de langage CLIP", "machine_learning_clip_model_description": "Le nom d'un modèle CLIP listé ici. Notez que vous devez réexécuter la tâche 'Recherche intelligente' pour toutes les images après avoir changé de modèle.", "machine_learning_duplicate_detection": "Détection des doublons", @@ -387,15 +395,15 @@ "admin_password": "Mot de passe Admin", "administration": "Administration", "advanced": "Avancé", - "advanced_settings_beta_timeline_subtitle": "Essayer la nouvelle application", - "advanced_settings_beta_timeline_title": "Timeline de la béta", - "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_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 tous les albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPÉRIMENTAL] Utiliser le filtre de synchronisation d'album alternatif", "advanced_settings_log_level_title": "Niveau de journalisation : {level}", "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des miniatures à partir de ressources locales. Activez ce paramètre pour charger des images externes à la place.", "advanced_settings_prefer_remote_title": "Préférer les images externes", "advanced_settings_proxy_headers_subtitle": "Ajoutez des en-têtes personnalisés à chaque requête réseau", "advanced_settings_proxy_headers_title": "En-têtes de proxy", + "advanced_settings_readonly_mode_subtitle": "Active le mode lecture seule, où les photos peuvent seulement être visualisées, et les actions comme les sélections multiples, le partage, la diffusion, la suppression sont désactivées. Activer/désactiver la lecture seule via l'image de l'utilisateur depuis l'écran d'accueil", + "advanced_settings_readonly_mode_title": "Mode lecture seule", "advanced_settings_self_signed_ssl_subtitle": "Permet d'ignorer la vérification du certificat SSL pour le point d'accès du serveur. Requis pour les certificats auto-signés.", "advanced_settings_self_signed_ssl_title": "Autoriser les certificats SSL auto-signés", "advanced_settings_sync_remote_deletions_subtitle": "Supprimer ou restaurer automatiquement un média sur cet appareil lorsqu'une action a été faite sur le web", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Êtes-vous sûr de vouloir supprimer {user} ?", "album_search_not_found": "Aucun album trouvé ne correspond à votre recherche", "album_share_no_users": "Il semble que vous ayez partagé cet album avec tous les utilisateurs ou que vous n'ayez aucun utilisateur avec lequel le partager.", + "album_summary": "Résumé de l'album", "album_updated": "Album mis à jour", "album_updated_setting_description": "Recevoir une notification par courriel lorsqu'un album partagé a de nouveaux médias", "album_user_left": "{album} quitté", @@ -461,7 +470,8 @@ "app_bar_signout_dialog_title": "Se déconnecter", "app_settings": "Paramètres de l'application", "appears_in": "Apparaît dans", - "archive": "Archiver", + "apply_count": "Appliquer ({count, number})", + "archive": "Archive", "archive_action_prompt": "{count} ajouté(s) à l'archive", "archive_or_unarchive_photo": "Archiver ou désarchiver une photo", "archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Élément restauré avec succès", "asset_skipped": "Sauté", "asset_skipped_in_trash": "À la corbeille", + "asset_trashed": "Média mis à la corbeille", + "asset_troubleshoot": "Dépannage de média", "asset_uploaded": "Envoyé", "asset_uploading": "Envoi…", "asset_viewer_settings_subtitle": "Modifier les paramètres du visualiseur photos", @@ -500,7 +512,7 @@ "assets": "Médias", "assets_added_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}}", "assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album", - "assets_added_to_albums_count": "{assetTotal, plural, one {# média ajouté} other {# médias ajoutés}} à {albumTotal} album(s)", + "assets_added_to_albums_count": "{assetTotal, plural, one {# média ajouté} other {# médias ajoutés}} à {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Le média ne peut pas être ajouté} other {Les médias ne peuvent pas être ajoutés}} à l'album", "assets_cannot_be_added_to_albums": "{count, plural, one {Le média ne peut être ajouté} other {Les médias ne peuvent être ajoutés}} à aucun des albums", "assets_count": "{count, plural, one {# média} other {# médias}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Lecture automatique d'un diaporama", "back": "Retour", "back_close_deselect": "Retournez en arrière, fermez ou désélectionnez", + "background_backup_running_error": "La sauvegarde en tâche de fond est actuellement en cours, impossible de démarrer une sauvegarde manuelle", "background_location_permission": "Permission de localisation en arrière plan", "background_location_permission_content": "Afin de pouvoir changer d'adresse en arrière plan, Immich doit avoir *en permanence* accès à la localisation précise, afin d'accéder au le nom du réseau Wi-Fi utilisé", + "background_options": "Options d'arrière-plan", "backup": "Sauvegarde", "backup_album_selection_page_albums_device": "Albums sur l'appareil ({count})", "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Sélectionner les albums", "backup_album_selection_page_selection_info": "Informations sur la sélection", "backup_album_selection_page_total_assets": "Total des éléments uniques", + "backup_albums_sync": "Sauvegarde de la synchronisation des albums", "backup_all": "Tout", "backup_background_service_backup_failed_message": "Échec de la sauvegarde des médias. Nouvelle tentative…", "backup_background_service_connection_failed_message": "Impossible de se connecter au serveur. Nouvelle tentative…", @@ -584,6 +599,7 @@ "backup_controller_page_turn_on": "Activer la sauvegarde au premier plan", "backup_controller_page_uploading_file_info": "Envoi des informations du fichier", "backup_err_only_album": "Impossible de retirer le seul album", + "backup_error_sync_failed": "Échec de la synchronisation. Impossible de démarrer la sauvegarde.", "backup_info_card_assets": "éléments", "backup_manual_cancelled": "Annulé", "backup_manual_in_progress": "Envoi déjà en cours. Réessayez plus tard", @@ -594,8 +610,6 @@ "backup_setting_subtitle": "Ajuster les paramètres d'envoi au premier et en arrière-plan", "backup_settings_subtitle": "Gérer les paramètres de téléversement", "backward": "Arrière", - "beta_sync": "Statut de la synchronisation béta", - "beta_sync_subtitle": "Gérer le nouveau système de synchronisation", "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", @@ -653,6 +667,8 @@ "change_pin_code": "Changer le code PIN", "change_your_password": "Changer votre mot de passe", "changed_visibility_successfully": "Visibilité modifiée avec succès", + "charging": "En charge", + "charging_requirement_mobile_backup": "La sauvegarde en tâche de fond nécessite que l'appareil soit en charge", "check_corrupt_asset_backup": "Vérifier la corruption des éléments enregistrés", "check_corrupt_asset_backup_button": "Vérifier", "check_corrupt_asset_backup_description": "Lancer cette vérification uniquement lorsque connecté à un réseau Wi-Fi et que tout le contenu a été enregistré. Cette procédure peut durer plusieurs minutes.", @@ -739,6 +755,7 @@ "create_user": "Créer un utilisateur", "created": "Créé", "created_at": "Créé à", + "creating_linked_albums": "Création des albums liés...", "crop": "Recadrer", "curated_object_page_title": "Objets", "current_device": "Appareil actuel", @@ -888,7 +905,9 @@ "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", + "error_getting_places": "Erreur à la récupération des lieux", "error_loading_image": "Erreur de chargement de l'image", + "error_loading_partners": "Erreur de récupération des partenaires : {error}", "error_saving_image": "Erreur : {error}", "error_tag_face_bounding_box": "Erreur lors de l'identification de visage - impossible de récupérer les coordonnées du cadre entourant le visage", "error_title": "Erreur - Quelque chose s'est mal passé", @@ -1053,6 +1072,7 @@ "favorites_page_no_favorites": "Aucun élément favori n'a été trouvé", "feature_photo_updated": "Photo de la personne mise à jour", "features": "Fonctionnalités", + "features_in_development": "Fonctionnalités en développement", "features_setting_description": "Gérer les fonctionnalités de l'application", "file_name": "Nom du fichier", "file_name_or_extension": "Nom du fichier ou extension", @@ -1073,12 +1093,15 @@ "gcast_enabled": "Diffusion Google Cast", "gcast_enabled_description": "Cette fonctionnalité charge des ressources externes depuis Google pour fonctionner.", "general": "Général", + "geolocation_instruction_location": "Cliquez sur un média avec des coordonnées GPS pour utiliser sa localisation, ou bien sélectionnez une localisation directement sur la carte", "get_help": "Obtenir de l'aide", "get_wifiname_error": "Impossible d'obtenir le nom du réseau wifi. Assurez-vous d'avoir donné les permissions nécessaires à l'application et que vous êtes connecté à un réseau wifi", "getting_started": "Commencer", "go_back": "Retour", "go_to_folder": "Dossier", "go_to_search": "Faire une recherche", + "gps": "GPS", + "gps_missing": "Pas de GPS", "grant_permission": "Accorder les permissions", "group_albums_by": "Grouper les albums par...", "group_country": "Grouper par pays", @@ -1214,6 +1237,7 @@ "local": "Local", "local_asset_cast_failed": "Impossible de caster un média qui n'a pas envoyé vers le serveur", "local_assets": "Média locaux", + "local_media_summary": "Résumé du média local", "local_network": "Réseau local", "local_network_sheet_info": "L'application va se connecter au serveur via cette URL quand l'appareil est connecté à ce réseau Wi-Fi", "location_permission": "Autorisation de localisation", @@ -1225,6 +1249,7 @@ "location_picker_longitude_hint": "Saisir la longitude ici", "lock": "Verrouiller", "locked_folder": "Dossier verrouillé", + "log_detail_title": "Niveau de journalisation", "log_out": "Se déconnecter", "log_out_all_devices": "Déconnecter tous les appareils", "logged_in_as": "Connecté en tant que {user}", @@ -1255,6 +1280,7 @@ "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 ?", + "logs": "Journaux", "longitude": "Longitude", "look": "Regarder", "loop_videos": "Vidéos en boucle", @@ -1262,6 +1288,7 @@ "main_branch_warning": "Vous utilisez une version de développement. Nous vous recommandons fortement d'utiliser une version stable !", "main_menu": "Menu principal", "make": "Marque", + "manage_geolocation": "Gérer la localisation", "manage_shared_links": "Gérer les liens partagés", "manage_sharing_with_partners": "Gérer le partage avec les partenaires", "manage_the_app_settings": "Gérer les paramètres de l'application", @@ -1296,6 +1323,7 @@ "mark_as_read": "Marquer comme lu", "marked_all_as_read": "Tout a été marqué comme lu", "matches": "Correspondances", + "matching_assets": "Médias correspondants", "media_type": "Type de média", "memories": "Souvenirs", "memories_all_caught_up": "Vous avez tout vu", @@ -1336,6 +1364,7 @@ "name_or_nickname": "Nom ou surnom", "network_requirement_photos_upload": "Utiliser les données mobile pour sauvegarder les photos", "network_requirement_videos_upload": "Utiliser les données mobile pour sauvegarder les vidéos", + "network_requirements": "Prérequis réseau", "network_requirements_updated": "Contraintes réseau modifiées, file d'attente de sauvegarde réinitialisée", "networking_settings": "Réseau", "networking_subtitle": "Gérer les adresses du serveur", @@ -1346,6 +1375,7 @@ "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_timeline": "Nouvelle vue chronologique", "new_user_created": "Nouvel utilisateur créé", "new_version_available": "NOUVELLE VERSION DISPONIBLE", "newest_first": "Récents en premier", @@ -1359,20 +1389,25 @@ "no_assets_message": "CLIQUEZ POUR ENVOYER VOTRE PREMIÈRE PHOTO", "no_assets_to_show": "Aucun élément à afficher", "no_cast_devices_found": "Aucun appareil de diffusion trouvé", + "no_checksum_local": "Aucune empreinte numerique disponible - impossible de récupérer les médias locaux", + "no_checksum_remote": "Aucune empreinte numérique disponible - impossible de récupérer les médias distants", "no_duplicates_found": "Aucun doublon n'a été trouvé.", "no_exif_info_available": "Aucune information exif disponible", "no_explore_results_message": "Envoyez plus de photos pour explorer votre bibliothèque.", "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_local_assets_found": "Aucun média local trouvé avec cette empreinte numerique", "no_locked_photos_message": "Les photos et vidéos du dossier verrouillé sont masqués et ne s'afficheront pas dans votre galerie ou la recherche.", "no_name": "Pas de nom", "no_notifications": "Pas de notification", "no_people_found": "Aucune personne correspondante trouvée", "no_places": "Pas de lieu", + "no_remote_assets_found": "Aucun média distant trouvé avec cette empreinte numerique", "no_results": "Aucun résultat", "no_results_description": "Essayez un synonyme ou un mot-clé plus général", "no_shared_albums_message": "Créer un album pour partager vos photos et vidéos avec les personnes de votre réseau", "no_uploads_in_progress": "Pas d'envoi en cours", + "not_available": "N/A", "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 précédemment envoyés, exécutez", @@ -1407,6 +1442,8 @@ "open_the_search_filters": "Ouvrir les filtres de recherche", "options": "Options", "or": "ou", + "organize_into_albums": "Organiser dans des albums", + "organize_into_albums_description": "Mettre les photos existantes dans des albums en utilisant les paramètres de synchronisation actuels", "organize_your_library": "Organiser votre bibliothèque", "original": "original", "other": "Autre", @@ -1492,6 +1529,7 @@ "port": "Port", "preferences_settings_subtitle": "Gérer les préférences de l'application", "preferences_settings_title": "Préférences", + "preparing": "Préparation", "preset": "Préréglage", "preview": "Aperçu", "previous": "Précédent", @@ -1508,6 +1546,7 @@ "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_readonly_mode": "Mode lecture seule activé. Faites un appui long sur l'image de l'utilisateur pour quitter.", "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}", @@ -1546,6 +1585,7 @@ "purchase_server_description_2": "Statut de contributeur", "purchase_server_title": "Serveur", "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", + "query_asset_id": "Obtenir l'ID du média", "queue_status": "{count}/{total} en file d'attente", "rating": "Étoile d'évaluation", "rating_clear": "Effacer l'évaluation", @@ -1553,6 +1593,9 @@ "rating_description": "Afficher l'évaluation EXIF dans le panneau d'information", "reaction_options": "Options de réaction", "read_changelog": "Lire les changements", + "readonly_mode_disabled": "Mode lecture seule désactivé", + "readonly_mode_enabled": "Mode lecture seule activé", + "ready_for_upload": "Téléchargement prêt", "reassign": "Réattribuer", "reassigned_assets_to_existing_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à {name, select, null {une personne existante} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à une nouvelle personne", @@ -1577,6 +1620,7 @@ "regenerating_thumbnails": "Regénération des miniatures", "remote": "À distance", "remote_assets": "Média à distance", + "remote_media_summary": "Résumé du média distant", "remove": "Supprimer", "remove_assets_album_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de l'album ?", "remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?", @@ -1629,12 +1673,13 @@ "restore_user": "Restaurer l'utilisateur", "restored_asset": "Média restauré", "resume": "Reprendre", + "resume_paused_jobs": "Reprendre {count, plural, one {la tâche en cours} other {les # tâches en cours}}", "retry_upload": "Réessayer l'envoi", "review_duplicates": "Consulter les doublons", "review_large_files": "Consulter les fichiers volumineux", "role": "Rôle", "role_editor": "Éditeur", - "role_viewer": "Visionneuse", + "role_viewer": "Visionneur", "running": "En cours", "save": "Sauvegarder", "save_to_gallery": "Enregistrer", @@ -1722,6 +1767,7 @@ "select_user_for_sharing_page_err_album": "Échec de la création de l'album", "selected": "Sélectionné", "selected_count": "{count, plural, one {# sélectionné} other {# sélectionnés}}", + "selected_gps_coordinates": "Coordonnées GPS sélectionnées", "send_message": "Envoyer un message", "send_welcome_email": "Envoyer un courriel de bienvenue", "server_endpoint": "Adresse du serveur", @@ -1850,6 +1896,7 @@ "show_slideshow_transition": "Afficher la transition du diaporama", "show_supporter_badge": "Badge de contributeur", "show_supporter_badge_description": "Afficher le badge de contributeur", + "show_text_search_menu": "Afficher le menu de recherche de texte", "shuffle": "Mélanger", "sidebar": "Barre latérale", "sidebar_display_description": "Afficher un lien vers la vue dans la barre latérale", @@ -1880,6 +1927,7 @@ "stacktrace": "Trace de la pile", "start": "Commencer", "start_date": "Date de début", + "start_date_before_end_date": "La date de début doit être avant la date de fin", "state": "Région", "status": "Statut", "stop_casting": "Arrêter la diffusion", @@ -1904,6 +1952,8 @@ "sync_albums_manual_subtitle": "Synchroniser toutes les vidéos et photos envoyées dans les albums sélectionnés", "sync_local": "Synchronisation locale", "sync_remote": "Synchronisation à distance", + "sync_status": "Statut de synchronisation", + "sync_status_subtitle": "Consulter et gérer le système de synchronisation", "sync_upload_album_setting_subtitle": "Créez et envoyez vos photos et vidéos dans les albums sélectionnés sur Immich", "tag": "Étiquette", "tag_assets": "Étiqueter les médias", @@ -1941,13 +1991,15 @@ "to_change_password": "Modifier le mot de passe", "to_favorite": "Ajouter aux favoris", "to_login": "Se connecter", + "to_multi_select": "pour faire une sélection multiple", "to_parent": "Aller au dossier parent", + "to_select": "pour faire une sélection", "to_trash": "Corbeille", "toggle_settings": "Inverser les paramètres", "total": "Total", "total_usage": "Utilisation globale", "trash": "Corbeille", - "trash_action_prompt": "{count} mis à la corbeille", + "trash_action_prompt": "{count} média(s) mis à la corbeille", "trash_all": "Tout supprimer", "trash_count": "Corbeille {count, number}", "trash_delete_asset": "Mettre à la corbeille/Supprimer un média", @@ -1961,6 +2013,7 @@ "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}}.", + "troubleshoot": "Dépannage", "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", @@ -1991,6 +2044,7 @@ "unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}", "untagged": "Sans étiquette", "up_next": "Suite", + "update_location_action_prompt": "Mettre à jour la localisation des {count} médias sélectionnés avec :", "updated_at": "Mis à jour à", "updated_password": "Mot de passe mis à jour", "upload": "Envoyer", @@ -2057,6 +2111,7 @@ "view_next_asset": "Voir le média suivant", "view_previous_asset": "Voir le média précédent", "view_qr_code": "Voir le QR code", + "view_similar_photos": "Afficher les photos similaires", "view_stack": "Afficher la pile", "view_user": "Voir l'utilisateur", "viewer_remove_from_stack": "Retirer de la pile", @@ -2075,5 +2130,6 @@ "yes": "Oui", "you_dont_have_any_shared_links": "Vous n'avez aucun lien partagé", "your_wifi_name": "Nom du réseau wifi", - "zoom_image": "Zoomer" + "zoom_image": "Zoomer", + "zoom_to_bounds": "Zoom sur la zone" } diff --git a/i18n/gl.json b/i18n/gl.json index ee1949250f..b6a59fadc7 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -14,6 +14,7 @@ "add_a_location": "Engadir unha ubicación", "add_a_name": "Engadir un nome", "add_a_title": "Engadir un título", + "add_birthday": "Engadir cumpleanos", "add_endpoint": "Engadir endpoint", "add_exclusion_pattern": "Engadir patrón de exclusión", "add_import_path": "Engadir ruta de importación", @@ -22,10 +23,14 @@ "add_partner": "Engadir compañeiro/a", "add_path": "Engadir ruta", "add_photos": "Engadir fotos", + "add_tag": "Engadir etiqueta", "add_to": "Engadir a…", "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_album_toggle": "Alternar selección para o {album}", + "add_to_albums": "Engadir en álbums", + "add_to_albums_count": "Engadir a {count} álbums", "add_to_shared_album": "Engadir ao álbum compartido", "add_url": "Engadir URL", "added_to_archive": "Engadido ao arquivo", @@ -33,17 +38,25 @@ "added_to_favorites_count": "Engadido {count, number} a favoritos", "admin": { "add_exclusion_pattern_description": "Engadir patróns de exclusión. Admítense caracteres comodín usando *, ** e ?. Para ignorar todos os ficheiros en calquera directorio chamado \"Raw\", emprega \"**/Raw/**\". Para ignorar todos os ficheiros que rematen en \".tif\", usa \"**/*.tif\". Para ignorar unha ruta absoluta, emprega \"/ruta/a/ignorar/**\".", + "admin_user": "Usuario administrador", "asset_offline_description": "Este activo da biblioteca externa xa non se atopa no disco e moveuse ao lixo. Se o ficheiro se moveu dentro da biblioteca, comproba a túa liña de tempo para o novo activo correspondente. Para restaurar este activo, asegúrate de que Immich poida acceder á ruta do ficheiro a continuación e escanee a biblioteca.", "authentication_settings": "Configuración de autenticación", "authentication_settings_description": "Xestionar contrasinal, OAuth e outras configuracións de autenticación", "authentication_settings_disable_all": "Estás seguro de que queres desactivar todos os métodos de inicio de sesión? O inicio de sesión desactivarase completamente.", "authentication_settings_reenable": "Para reactivalo, use un Comando de servidor.", "background_task_job": "Tarefas en segundo plano", - "backup_database": "Copia de seguridade da base de datos", - "backup_database_enable_description": "Activar copias de seguridade da base de datos", + "backup_database": "Crear un vertedoiro de base de datos", + "backup_database_enable_description": "Activar o vertedoiro de copias de seguridade da base de datos", "backup_keep_last_amount": "Cantidade de copias de seguridade anteriores a conservar", + "backup_onboarding_1_description": "Copia no exterior na nube ou noutra localización física.", + "backup_onboarding_2_description": "Copias locais en diferentes dispositivos. Isto inclue os arquivos principais e as copias de esos arquivos localmente.", + "backup_onboarding_3_description": "copias totais da tua información, incluindo os arquivos orixinais. Isto inclue 1 copia externa e 2 copias locais.", + "backup_onboarding_description": "Unha estratexia de copia 3-2-1 é recomendada para protexer os teus datos. Deberías gardar copias das túas fotos/videos subidas así como da base de datos de Immich como unha solución de seguridade.", + "backup_onboarding_footer": "Pra máis información sobre copias de seguridade de Immich, por favor use a seguinte ligazón de documentación.", + "backup_onboarding_parts_title": "Unha copia de seguridade 3-2-1 inclue:", + "backup_onboarding_title": "Copia de seguridade", "backup_settings": "Configuración da copia de seguridade", - "backup_settings_description": "Xestionar a configuración da copia de seguridade da base de datos", + "backup_settings_description": "Xestionar a configuración do volcado da base de datos", "cleared_jobs": "Traballos borrados para: {job}", "config_set_by_file": "A configuración establécese actualmente mediante un ficheiro de configuración", "confirm_delete_library": "Estás seguro de que queres eliminar a biblioteca {library}?", @@ -110,6 +123,13 @@ "logging_enable_description": "Activar rexistro", "logging_level_description": "Cando estea activado, que nivel de rexistro usar.", "logging_settings": "Rexistro", + "machine_learning_availability_checks": "Comprobacións de dispoñibilidade", + "machine_learning_availability_checks_description": "Detectar automáticamente e preferir servidores de aprendizaxe profunda dispoñibles", + "machine_learning_availability_checks_enabled": "Activar comprobacións de dispoñibilidade", + "machine_learning_availability_checks_interval": "Intervalo de comprobación", + "machine_learning_availability_checks_interval_description": "Intervalo en milisegundos entre comprobacións de dispoñibilidade", + "machine_learning_availability_checks_timeout": "Tempo de espera da solicitude", + "machine_learning_availability_checks_timeout_description": "Tempo de espera en milisegundos para as comprobación de dispoñibilidade", "machine_learning_clip_model": "Modelo CLIP", "machine_learning_clip_model_description": "O nome dun modelo CLIP listado aquí. Ten en conta que debe volver executar o traballo 'Busca Intelixente' para todas as imaxes ao cambiar un modelo.", "machine_learning_duplicate_detection": "Detección de duplicados", @@ -164,6 +184,19 @@ "metadata_settings_description": "Xestionar a configuración de metadatos", "migration_job": "Migración", "migration_job_description": "Migrar miniaturas de activos e caras á última estrutura de cartafoles", + "nightly_tasks_cluster_faces_setting_description": "Executar recoñecemento facial nas novas caras detectadas", + "nightly_tasks_cluster_new_faces_setting": "Agrupar novas caras", + "nightly_tasks_database_cleanup_setting": "Tarefas de limpeza da base de datos", + "nightly_tasks_database_cleanup_setting_description": "Limpar información vella e obsoleta da base de datos", + "nightly_tasks_generate_memories_setting": "Xerar memorias", + "nightly_tasks_generate_memories_setting_description": "Crear novas memorias dende os recursos", + "nightly_tasks_missing_thumbnails_setting": "Xerar as miniaturas que faltan", + "nightly_tasks_missing_thumbnails_setting_description": "Encolar arquivos sin miniaturas para a xeración das miniaturas", + "nightly_tasks_settings": "Configuración das tarefas nocturnas", + "nightly_tasks_settings_description": "Administrar as tarefas nocturnas", + "nightly_tasks_start_time_setting": "Tempo de inicio", + "nightly_tasks_start_time_setting_description": "O tempo no que o servidor comeza a executar as tarefas nocturnas", + "nightly_tasks_sync_quota_usage_setting": "Sincronizar uso de cuota", "no_paths_added": "Non se engadiron rutas", "no_pattern_added": "Non se engadiu ningún padrón", "note_apply_storage_label_previous_assets": "Nota: Para aplicar a Etiqueta de Almacenamento a activos cargados previamente, execute o", diff --git a/i18n/he.json b/i18n/he.json index 4483b183b7..5f71e2c8a8 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -28,6 +28,9 @@ "add_to_album": "הוספה לאלבום", "add_to_album_bottom_sheet_added": "נוסף ל {album}", "add_to_album_bottom_sheet_already_exists": "כבר ב {album}", + "add_to_album_toggle": "החלפת מצב בחירה עבור {album}", + "add_to_albums": "הוספה לאלבומים", + "add_to_albums_count": "Add to albums ({count})", "add_to_shared_album": "הוספה לאלבום משותף", "add_url": "הוספת קישור", "added_to_archive": "נוסף לארכיון", @@ -120,6 +123,13 @@ "logging_enable_description": "אפשר רישום ביומן", "logging_level_description": "כאשר פועל, באיזה רמת יומן לתעד.", "logging_settings": "רישום ביומן", + "machine_learning_availability_checks": "בדיקת זמינות", + "machine_learning_availability_checks_description": "זהה ותעדף אוטומטית שרתי למידת מכונה זמינים", + "machine_learning_availability_checks_enabled": "הפעלת בדיקות זמינות", + "machine_learning_availability_checks_interval": "תזמון בדיקה", + "machine_learning_availability_checks_interval_description": "מרווח זמן במילישניות בין בדיקות זמינות", + "machine_learning_availability_checks_timeout": "פסק זמן לבקשה", + "machine_learning_availability_checks_timeout_description": "פסק זמן במילישניות עבור בדיקות זמינות", "machine_learning_clip_model": "מודל CLIP", "machine_learning_clip_model_description": "שמו של מודל CLIP רשום כאן. שים לב שעליך להפעיל מחדש את המשימה 'חיפוש חכם' עבור כל התמונות בעת שינוי מודל.", "machine_learning_duplicate_detection": "איתור כפילויות", @@ -355,6 +365,9 @@ "trash_number_of_days_description": "מספר הימים לשמירה של תמונות באשפה לפני הסרתם לצמיתות", "trash_settings": "הגדרות האשפה", "trash_settings_description": "ניהול הגדרות האשפה", + "unlink_all_oauth_accounts": "ביטול קישור כל חשבונות OAuth", + "unlink_all_oauth_accounts_description": "זכור לבטל את הקישור של כל חשבונות ה-OAuth לפני ההגירה לספק חדש.", + "unlink_all_oauth_accounts_prompt": "האם באמת ברצונך לבטל את הקישור של כל חשבונות ה-OAuth? פעולה זו תאפס את מזהה ה-OAuth עבור כל משתמש ואינה ניתנת לביטול.", "user_cleanup_job": "ניקוי משתמשים", "user_delete_delay": "החשבון והתמונות של {user} יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.", "user_delete_delay_settings": "עיכוב מחיקה", @@ -381,8 +394,6 @@ "admin_password": "סיסמת מנהל", "administration": "ניהול", "advanced": "מתקדם", - "advanced_settings_beta_timeline_subtitle": "נסה את חווית האפליקציה החדשה", - "advanced_settings_beta_timeline_title": "ציר זמן (בטא)", "advanced_settings_enable_alternate_media_filter_subtitle": "השתמש באפשרות זו כדי לסנן מדיה במהלך הסנכרון לפי קריטריונים חלופיים. מומלץ להשתמש בזה רק אם יש בעיה בזיהוי כל האלבומים באפליקציה.", "advanced_settings_enable_alternate_media_filter_title": "[ניסיוני] השתמש במסנן סנכרון אלבום חלופי שמבכשיר", "advanced_settings_log_level_title": "רמת רישום ביומן: {level}", @@ -390,6 +401,8 @@ "advanced_settings_prefer_remote_title": "העדף תמונות מרוחקות", "advanced_settings_proxy_headers_subtitle": "הגדר proxy headers שהיישום צריך לשלוח עם כל בקשת רשת", "advanced_settings_proxy_headers_title": "כותרות פרוקסי", + "advanced_settings_readonly_mode_subtitle": "מאפשר את מצב לקריאה בלבד בו התמונות ניתנות לצפייה בלבד, דברים כמו בחירת תמונות מרובות, שיתוף, שידור, מחיקה הם כולם מושבתים. אפשר/השבת מצב לקריאה בלבד באמצעות יצגן המשתמש מהמסך הראשי", + "advanced_settings_readonly_mode_title": "מצב לקריאה בלבד", "advanced_settings_self_signed_ssl_subtitle": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית.", "advanced_settings_self_signed_ssl_title": "התר תעודות SSL בחתימה עצמית", "advanced_settings_sync_remote_deletions_subtitle": "מחק או שחזר תמונה במכשיר זה באופן אוטומטי כאשר פעולה זו נעשית בדפדפן", @@ -417,6 +430,7 @@ "album_remove_user_confirmation": "האם באמת ברצונך להסיר את {user}?", "album_search_not_found": "לא נמצאו אלבומים התואמים לחיפוש שלך", "album_share_no_users": "נראה ששיתפת את האלבום הזה עם כל המשתמשים או שאין לך אף משתמש לשתף איתו.", + "album_summary": "תקציר אלבום", "album_updated": "אלבום עודכן", "album_updated_setting_description": "קבל הודעת דוא\"ל כאשר לאלבום משותף יש תמונות חדשות", "album_user_left": "עזב את {album}", @@ -455,6 +469,7 @@ "app_bar_signout_dialog_title": "התנתק", "app_settings": "הגדרות יישום", "appears_in": "מופיע ב", + "apply_count": "החל ({count, number})", "archive": "ארכיון", "archive_action_prompt": "{count} נוספו לארכיון", "archive_or_unarchive_photo": "העבר תמונה לארכיון או הוצא אותה משם", @@ -487,6 +502,8 @@ "asset_restored_successfully": "תמונה שוחזרה בהצלחה", "asset_skipped": "דילג", "asset_skipped_in_trash": "באשפה", + "asset_trashed": "התמונה הועברה לאשפה", + "asset_troubleshoot": "פתרון בעיות בתמונות", "asset_uploaded": "הועלה", "asset_uploading": "מעלה…", "asset_viewer_settings_subtitle": "ניהול הגדרות מציג הגלריה שלך", @@ -494,7 +511,9 @@ "assets": "תמונות וסרטונים", "assets_added_count": "{count, plural, one {נוספה תומנה #} other {נוספו # תמונות}}", "assets_added_to_album_count": "{count, plural, one {נוספה תמונה #} other {נוספו # תמונות}} לאלבום", + "assets_added_to_albums_count": "{assetTotal, plural, one {נוסף פריט #} other {נוספו # פריטים}} אל {albumTotal, plural, one {אלבום #} other {# אלבומים}}", "assets_cannot_be_added_to_album_count": "לא ניתן להוסיף את ה{count, plural, one {תמונה} other {תמונות}} לאלבום", + "assets_cannot_be_added_to_albums": "לא ניתן להוסיף {count, plural, one {פריט} other {פריטים}} לאף אחד מהאלבומים", "assets_count": "{count, plural, one {תמונה #} other {# תמונות}}", "assets_deleted_permanently": "{count} תמונות נמחקו לצמיתות", "assets_deleted_permanently_from_server": "{count} תמונות נמחקו לצמיתות משרת ה-Immich", @@ -511,14 +530,17 @@ "assets_trashed_count": "{count, plural, one {תמונה # הושלכה} other {# תמונות הושלכו}} לאשפה", "assets_trashed_from_server": "{count} תמונות הועברו לאשפה מהשרת", "assets_were_part_of_album_count": "{count, plural, one {תמונה הייתה} other {תמונות היו}} כבר חלק מהאלבום", + "assets_were_part_of_albums_count": "{count, plural, one {הפריט כבר היה} other {הפריטים כבר היו}} חלק מהאלבומים", "authorized_devices": "מכשירים מורשים", "automatic_endpoint_switching_subtitle": "התחבר מקומית דרך אינטרנט אלחוטי ייעודי כאשר זמין והשתמש בחיבורים חלופיים במקומות אחרים", "automatic_endpoint_switching_title": "החלפת כתובת אוטומטית", "autoplay_slideshow": "מצגת תמונות אוטומטית", "back": "חזרה", "back_close_deselect": "חזור, סגור, או בטל בחירה", + "background_backup_running_error": "גיבוי ברקע פועל כעת, לא ניתן להפעיל גיבוי ידני", "background_location_permission": "הרשאת מיקום ברקע", "background_location_permission_content": "כדי להחליף רשתות בעת ריצה ברקע, היישום צריך *תמיד* גישה למיקום מדויק על מנת לקרוא את השם של רשת האינטרנט האלחוטי", + "background_options": "אפשרויות רקע", "backup": "גיבוי", "backup_album_selection_page_albums_device": "({count}) אלבומים במכשיר", "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פעמיים כדי להחריג", @@ -526,6 +548,7 @@ "backup_album_selection_page_select_albums": "בחירת אלבומים", "backup_album_selection_page_selection_info": "פרטי בחירה", "backup_album_selection_page_total_assets": "סה״כ תמונות ייחודיות", + "backup_albums_sync": "סנכרון אלבומי גיבוי", "backup_all": "הכל", "backup_background_service_backup_failed_message": "נכשל בגיבוי תמונות. מנסה שוב…", "backup_background_service_connection_failed_message": "נכשל בהתחברות לשרת. מנסה שוב…", @@ -585,8 +608,6 @@ "backup_setting_subtitle": "ניהול הגדרות העלאת רקע וחזית", "backup_settings_subtitle": "נהל הגדרות העלאה", "backward": "אחורה", - "beta_sync": "סטטוס סנכרון (בטא)", - "beta_sync_subtitle": "נהל את מערכת הסנכרון החדשה", "biometric_auth_enabled": "אימות ביומטרי הופעל", "biometric_locked_out": "גישה לאימות הביומטרי נחסמה", "biometric_no_options": "אין אפשרויות זמינות עבור אימות ביומטרי", @@ -644,6 +665,8 @@ "change_pin_code": "שנה קוד PIN", "change_your_password": "החלף את הסיסמה שלך", "changed_visibility_successfully": "הנראות שונתה בהצלחה", + "charging": "טוען", + "charging_requirement_mobile_backup": "גיבוי ברקע דורש טעינה של המכשיר", "check_corrupt_asset_backup": "בדוק גיבויים פגומים של תמונות", "check_corrupt_asset_backup_button": "בצע בדיקה", "check_corrupt_asset_backup_description": "הרץ בדיקה זו רק על Wi-Fi ולאחר שכל התמונות גובו. ההליך עשוי לקחת כמה דקות.", @@ -749,6 +772,7 @@ "date_of_birth_saved": "תאריך לידה נשמר בהצלחה", "date_range": "טווח תאריכים", "day": "יום", + "days": "ימים", "deduplicate_all": "ביטול כל הכפילויות", "deduplication_criteria_1": "גודל תמונה בבתים", "deduplication_criteria_2": "כמות נתוני EXIF", @@ -832,10 +856,13 @@ "duration": "משך זמן", "edit": "ערוך", "edit_album": "ערוך אלבום", - "edit_avatar": "ערוך תמונת פרופיל", + "edit_avatar": "ערוך יצגן", "edit_birthday": "עריכת יום הולדת", "edit_date": "ערוך תאריך", "edit_date_and_time": "ערוך תאריך ושעה", + "edit_date_and_time_action_prompt": "{count} תאריך ושעה נערכו", + "edit_date_and_time_by_offset": "שינוי תאריך לפי קיזוז", + "edit_date_and_time_by_offset_interval": "טווח תאריכים חדש: {from} עד {to}", "edit_description": "ערוך תיאור", "edit_description_prompt": "אנא בחר תיאור חדש:", "edit_exclusion_pattern": "ערוך דפוס החרגה", @@ -875,7 +902,9 @@ "error": "שגיאה", "error_change_sort_album": "שינוי סדר מיון אלבום נכשל", "error_delete_face": "שגיאה במחיקת פנים מתמונה", + "error_getting_places": "שגיאה בקבלת מקומות", "error_loading_image": "שגיאה בטעינת התמונה", + "error_loading_partners": "שגיאה בטעינת שותפים: {error}", "error_saving_image": "שגיאה: {error}", "error_tag_face_bounding_box": "שגיאה בתיוג הפנים – לא ניתן לקבל את קואורדינטות המסגרת", "error_title": "שגיאה - משהו השתבש", @@ -908,6 +937,7 @@ "failed_to_load_notifications": "אירעה שגיאה בעת טעינת ההתראות", "failed_to_load_people": "נכשל באחזור אנשים", "failed_to_remove_product_key": "הסרת מפתח מוצר נכשלה", + "failed_to_reset_pin_code": "איפוס קוד PIN נכשל", "failed_to_stack_assets": "יצירת ערימת תמונות נכשלה", "failed_to_unstack_assets": "ביטול ערימת תמונות נכשלה", "failed_to_update_notification_status": "שגיאה בעדכון ההתראה", @@ -916,6 +946,7 @@ "paths_validation_failed": "{paths, plural, one {נתיב # נכשל} other {# נתיבים נכשלו}} אימות", "profile_picture_transparent_pixels": "תמונות פרופיל אינן יכולות לכלול פיקסלים שקופים. נא להגדיל ו/או להזיז את התמונה.", "quota_higher_than_disk_size": "הגדרת מכסה גבוהה יותר מגודל הדיסק", + "something_went_wrong": "משהו השתבש", "unable_to_add_album_users": "לא ניתן להוסיף משתמשים לאלבום", "unable_to_add_assets_to_shared_link": "לא ניתן להוסיף תמונות לקישור משותף", "unable_to_add_comment": "לא ניתן להוסיף תגובה", @@ -1038,6 +1069,7 @@ "favorites_page_no_favorites": "לא נמצאו תמונות מועדפים", "feature_photo_updated": "תמונה מייצגת עודכנה", "features": "תכונות", + "features_in_development": "תכונות בפיתוח", "features_setting_description": "ניהול תכונות היישום", "file_name": "שם הקובץ", "file_name_or_extension": "שם קובץ או סיומת", @@ -1047,21 +1079,26 @@ "filter_people": "סנן אנשים", "filter_places": "סינון מקומות", "find_them_fast": "מצא אותם מהר לפי שם עם חיפוש", + "first": "ראשון", "fix_incorrect_match": "תקן התאמה שגויה", "folder": "תיקיה", "folder_not_found": "תיקיה לא נמצאה", "folders": "תיקיות", "folders_feature_description": "עיון בתצוגת התיקייה עבור התמונות והסרטונים שבמערכת הקבצים", + "forgot_pin_code_question": "שחכת את ה-PIN שלך?", "forward": "קדימה", "gcast_enabled": "Google Cast", "gcast_enabled_description": "תכונה זאת טוענת משאבים חיצוניים מגוגל בכדי לפעול.", "general": "כללי", + "geolocation_instruction_location": "לחץ על פריט עם קואורדינטות GPS כדי להשתמש במיקומו, או בחר מיקום ישירות מהמפה", "get_help": "קבל עזרה", "get_wifiname_error": "לא היה ניתן לקבל את שם האינטרנט האלחוטי שלך. יש לודא שהענקת את ההרשאות הדרושות ושאת/ה מחובר/ת לרשת אינטרנט אלחוטי", "getting_started": "תחילת העבודה", "go_back": "חזור", "go_to_folder": "עבור לתיקיה", "go_to_search": "עבור לחיפוש", + "gps": "GPS", + "gps_missing": "אין GPS", "grant_permission": "להעניק הרשאה", "group_albums_by": "קבץ אלבומים לפי..", "group_country": "קבץ לפי מדינה", @@ -1072,7 +1109,7 @@ "haptic_feedback_switch": "אפשר משוב ברטט", "haptic_feedback_title": "משוב ברטט", "has_quota": "יש מכסה", - "hash_asset": "גיבוב תמונה", + "hash_asset": "גבב פריט", "hashed_assets": "תמונות מגובבות", "hashing": "מגבב", "header_settings_add_header_tip": "הוסף כותרת", @@ -1106,8 +1143,9 @@ "home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 תמונות בכל פעם, מדלג", "host": "מארח", "hour": "שעה", + "hours": "שעות", "id": "מזהה", - "idle": "ממתין", + "idle": "במצב סרק", "ignore_icloud_photos": "התעלם מתמונות iCloud", "ignore_icloud_photos_description": "תמונות שמאוחסנות ב-iCloud לא יועלו לשרת", "image": "תמונה", @@ -1166,10 +1204,12 @@ "language_search_hint": "חפש שפות...", "language_setting_description": "בחר את השפה המועדפת עליך", "large_files": "קבצים גדולים", + "last": "אחרון", "last_seen": "נראה לאחרונה", "latest_version": "גרסה עדכנית ביותר", "latitude": "קו רוחב", "leave": "לעזוב", + "leave_album": "עזיבת אלבום", "lens_model": "דגם עדשה", "let_others_respond": "אפשר לאחרים להגיב", "level": "רמה", @@ -1183,6 +1223,7 @@ "library_page_sort_title": "כותרת אלבום", "licenses": "רישיונות", "light": "בהיר", + "like": "אהבתי", "like_deleted": "לייק נמחק", "link_motion_video": "קשר סרטון תנועה", "link_to_oauth": "קישור ל-OAuth", @@ -1193,6 +1234,7 @@ "local": "מקומי", "local_asset_cast_failed": "לא ניתן לשדר תמונה שלא הועלתה לשרת", "local_assets": "תמונות מקומיות", + "local_media_summary": "סיכום של מדיה מקומית", "local_network": "רשת מקומית", "local_network_sheet_info": "היישום יתחבר לשרת דרך הכתובת הזאת כאשר משתמשים ברשת האינטרנט האלחוטי שמצוינת", "location_permission": "הרשאת מיקום", @@ -1204,6 +1246,7 @@ "location_picker_longitude_hint": "הזן את קו האורך שלך כאן", "lock": "נעל", "locked_folder": "תיקיה נעולה", + "log_detail_title": "פרטי יומן", "log_out": "התנתק", "log_out_all_devices": "התנתק מכל המכשירים", "logged_in_as": "מחובר כ {user}", @@ -1234,6 +1277,7 @@ "login_password_changed_success": "סיסמה עודכנה בהצלחה", "logout_all_device_confirmation": "האם באמת ברצונך להתנתק מכל המכשירים?", "logout_this_device_confirmation": "האם באמת ברצונך להתנתק מהמכשיר הזה?", + "logs": "יומנים", "longitude": "קו אורך", "look": "מראה", "loop_videos": "הפעלה חוזרת של סרטונים", @@ -1241,6 +1285,7 @@ "main_branch_warning": "הגרסה המותקנת היא גרסת פיתוח; אנחנו ממליצים בחום להשתמש בגרסה יציבה!", "main_menu": "תפריט ראשי", "make": "תוצרת", + "manage_geolocation": "נהל מיקום", "manage_shared_links": "ניהול קישורים משותפים", "manage_sharing_with_partners": "ניהול שיתוף עם שותפים", "manage_the_app_settings": "ניהול הגדרות האפליקציה", @@ -1249,7 +1294,7 @@ "manage_your_devices": "ניהול המכשירים המחוברים שלך", "manage_your_oauth_connection": "ניהול חיבור ה-OAuth שלך", "map": "מפה", - "map_assets_in_bounds": "{count, plural, one {תמונה #} other {# תמונות}}", + "map_assets_in_bounds": "{count, plural, =0 {אין תמונות באזור זה} one {תמונה #} other {# תמונות}}", "map_cannot_get_user_location": "לא ניתן לקבוע את מיקום המשתמש", "map_location_dialog_yes": "כן", "map_location_picker_page_use_location": "השתמש במיקום הזה", @@ -1275,6 +1320,7 @@ "mark_as_read": "סמן כנקרא", "marked_all_as_read": "כל ההתראות סומנו כנקראו", "matches": "התאמות", + "matching_assets": "תמונות תואמות", "media_type": "סוג מדיה", "memories": "זכרונות", "memories_all_caught_up": "ראית הכל", @@ -1293,6 +1339,7 @@ "merged_people_count": "{count, plural, one {אדם # מוזג} other {# אנשים מוזגו}}", "minimize": "מזער", "minute": "דקה", + "minutes": "דקות", "missing": "חסרים", "model": "דגם", "month": "חודש", @@ -1314,6 +1361,7 @@ "name_or_nickname": "שם או כינוי", "network_requirement_photos_upload": "השתמש בנתונים ניידים לגיבוי תמונות", "network_requirement_videos_upload": "השתמש בנתונים ניידים לגיבוי סרטונים", + "network_requirements": "דרישות רשת", "network_requirements_updated": "דרישות הרשת השתנו, תור הגיבוי אופס", "networking_settings": "רשת", "networking_subtitle": "ניהול הגדרות נקודת קצה שרת", @@ -1324,6 +1372,7 @@ "new_person": "אדם חדש", "new_pin_code": "קוד PIN חדש", "new_pin_code_subtitle": "זאת הפעם הראשונה שנכנסת לתיקיה הנעולה. צור קוד PIN כדי לאבטח את הגישה לדף זה", + "new_timeline": "ציר הזמן החדש", "new_user_created": "משתמש חדש נוצר", "new_version_available": "גרסה חדשה זמינה", "newest_first": "החדש ביותר ראשון", @@ -1337,20 +1386,25 @@ "no_assets_message": "לחץ כדי להעלות את התמונה הראשונה שלך", "no_assets_to_show": "אין תמונות להצגה", "no_cast_devices_found": "לא נמצאו מכשירי שידור", + "no_checksum_local": "אין Checksum זמין - לא ניתן לאחזר תמונות מקומיות", + "no_checksum_remote": "אין Checksum זמין - לא ניתן לאחזר תמונות מהשרת", "no_duplicates_found": "לא נמצאו כפילויות.", "no_exif_info_available": "אין מידע זמין על מטא-נתונים (exif)", "no_explore_results_message": "העלה תמונות נוספות כדי לחקור את האוסף שלך.", "no_favorites_message": "הוסף מועדפים כדי למצוא במהירות את התמונות והסרטונים הכי טובים שלך", "no_libraries_message": "צור ספרייה חיצונית כדי לראות את התמונות והסרטונים שלך", + "no_local_assets_found": "לא נמצאו תמונות עם Checksum זהה", "no_locked_photos_message": "תמונות וסרטונים בתיקייה הנעולה מוסתרים ולא יופיעו בזמן הגלישה או החיפוש בספרייה שלך.", "no_name": "אין שם", "no_notifications": "אין התראות", "no_people_found": "לא נמצאו אנשים תואמים", "no_places": "אין מקומות", + "no_remote_assets_found": "לא נמצאו תמונות בשרת עם Checksum זהה", "no_results": "אין תוצאות", "no_results_description": "נסה להשתמש במילה נרדפת או במילת מפתח יותר כללית", "no_shared_albums_message": "צור אלבום כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך", "no_uploads_in_progress": "אין העלאות בתהליך", + "not_available": "לא רלוונטי", "not_in_any_album": "לא בשום אלבום", "not_selected": "לא נבחרו", "note_apply_storage_label_to_previously_uploaded assets": "הערה: כדי להחיל את תווית האחסון על תמונות שהועלו בעבר, הפעל את", @@ -1366,6 +1420,7 @@ "oauth": "OAuth", "official_immich_resources": "מקורות רשמיים של Immich", "offline": "לא מקוון", + "offset": "קיזוז", "ok": "בסדר", "oldest_first": "הישן ביותר ראשון", "on_this_device": "במכשיר הזה", @@ -1384,6 +1439,8 @@ "open_the_search_filters": "פתח את מסנני החיפוש", "options": "אפשרויות", "or": "או", + "organize_into_albums": "ארגן בתוך אלבומים", + "organize_into_albums_description": "שים תמונות קיימות בתוך אלבומים באמצעות הגדרות הסנכרון הנוכחיות", "organize_your_library": "ארגן את הספרייה שלך", "original": "מקורי", "other": "אחר", @@ -1443,6 +1500,9 @@ "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת ליישום לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות.", "permission_onboarding_request": "היישום דורש הרשאה כדי לראות את התמונות והסרטונים שלך.", "person": "אדם", + "person_age_months": "בגיל {months, plural, one {חודש #} other {# חודשים}}", + "person_age_year_months": "בגיל שנה ו{months, plural, one {חודש #} other {# חודשים}}", + "person_age_years": "בגיל {years, plural, other {# שנים}}", "person_birthdate": "נולד בתאריך {date}", "person_hidden": "{name}{hidden, select, true { (מוסתר)} other {}}", "photo_shared_all_users": "נראה ששיתפת את התמונות שלך עם כל המשתמשים או שאין לך אף משתמש לשתף איתו.", @@ -1466,6 +1526,7 @@ "port": "יציאה", "preferences_settings_subtitle": "ניהול העדפות יישום", "preferences_settings_title": "העדפות", + "preparing": "בהכנה", "preset": "הגדרות קבועות מראש", "preview": "תצוגה מקדימה", "previous": "הקודם", @@ -1482,6 +1543,7 @@ "profile_drawer_client_out_of_date_minor": "גרסת היישום לנייד מיושנת. נא לעדכן לגרסה המשנית האחרונה.", "profile_drawer_client_server_up_to_date": "היישום והשרת מעודכנים", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "מצב לקריאה בלבד מופעל. לחץ לחיצה ארוכה על סמל היצגן של המשתמש כדי לצאת.", "profile_drawer_server_out_of_date_major": "השרת אינו מעודכן. נא לעדכן לגרסה הראשית האחרונה.", "profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה המשנית האחרונה.", "profile_image_of_user": "תמונת פרופיל של {user}", @@ -1520,17 +1582,21 @@ "purchase_server_description_2": "מעמד תומך", "purchase_server_title": "שרת", "purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת", - "queue_status": "בתור {count}/{total}", + "query_asset_id": "שאילתה על מזהה הפריט", + "queue_status": "{count} מתוך {total} עומדים בתור", "rating": "דירוג כוכב", "rating_clear": "נקה דירוג", "rating_count": "{count, plural, one {כוכב #} other {# כוכבים}}", "rating_description": "הצג את דירוג ה-EXIF בלוח המידע", "reaction_options": "אפשרויות הגבה", "read_changelog": "קרא את יומן השינויים", - "reassign": "הקצה מחדש", + "readonly_mode_disabled": "מצב לקריאה בלבד מושבת", + "readonly_mode_enabled": "מצב לקריאה בלבד מופעל", + "ready_for_upload": "מוכן להעלאה", + "reassign": "הקצאה מחדש", "reassigned_assets_to_existing_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש אל {name, select, null {אדם קיים} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש לאדם חדש", - "reassing_hint": "הקצה תמונות שנבחרו לאדם קיים", + "reassing_hint": "הקצאת תמונות שנבחרו לאדם קיים", "recent": "חדש", "recent-albums": "אלבומים אחרונים", "recent_searches": "חיפושים אחרונים", @@ -1538,11 +1604,11 @@ "recently_added_page_title": "נוסף לאחרונה", "recently_taken": "צולם לאחרונה", "recently_taken_page_title": "צולם לאחרונה", - "refresh": "רענן", - "refresh_encoded_videos": "רענן סרטונים מקודדים", - "refresh_faces": "רענן פנים", - "refresh_metadata": "רענן מטא-נתונים", - "refresh_thumbnails": "רענן תמונות ממוזערות", + "refresh": "רענון", + "refresh_encoded_videos": "רענון סרטונים מקודדים", + "refresh_faces": "רענון פנים", + "refresh_metadata": "רענון מטא-נתונים", + "refresh_thumbnails": "רענון תמונות ממוזערות", "refreshed": "רוענן", "refreshes_every_file": "קורא מחדש את כל הקבצים הקיימים והחדשים", "refreshing_encoded_video": "מרענן סרטון מקודד", @@ -1551,15 +1617,16 @@ "regenerating_thumbnails": "מחדש תמונות ממוזערות", "remote": "מרוחק", "remote_assets": "תמונות מרוחקות", - "remove": "הסר", + "remote_media_summary": "תקציר תמונות מרוחקות", + "remove": "הסרה", "remove_assets_album_confirmation": "האם באמת ברצונך להסיר {count, plural, one {תמונה #} other {# תמונות}} מהאלבום?", - "remove_assets_shared_link_confirmation": "האם אתה בטוח שברצונך להסיר {count, plural, one {תמונה #} other {# תמונות}} מהקישור המשותף הזה?", + "remove_assets_shared_link_confirmation": "האם ברצונך להסיר {count, plural, one {תמונה #} other {# תמונות}} מהקישור המשותף הזה?", "remove_assets_title": "להסיר תמונות?", - "remove_custom_date_range": "הסר טווח תאריכים מותאם", - "remove_deleted_assets": "הסר קבצים לא מקוונים", - "remove_from_album": "הסר מאלבום", + "remove_custom_date_range": "הסרת טווח תאריכים מותאם", + "remove_deleted_assets": "הסרת קבצים לא מקוונים", + "remove_from_album": "הסרה מאלבום", "remove_from_album_action_prompt": "{count} הוסרו מהאלבום", - "remove_from_favorites": "הסר מהמועדפים", + "remove_from_favorites": "הסרה מהמועדפים", "remove_from_lock_folder_action_prompt": "{count} הוסרו מהתיקייה הנעולה", "remove_from_locked_folder": "הסר מהתיקייה הנעולה", "remove_from_locked_folder_confirmation": "האם אתה בטוח שברצונך להעביר את התמונות והסרטונים האלה מחוץ לתיקייה הנעולה? הם יהיו מוצגים בספרייה שלך.", @@ -1588,6 +1655,9 @@ "reset_password": "איפוס סיסמה", "reset_people_visibility": "אפס את נראות האנשים", "reset_pin_code": "אפס קוד PIN", + "reset_pin_code_description": "אם שחכת את קוד ה-PIN שלך, באפשרותך ליצור קשר עם מנהל השרת כדי לאפס אותו", + "reset_pin_code_success": "קוד ה-PIN אופס בהצלחה", + "reset_pin_code_with_password": "באפשרותך תמיד לאפס את קוד ה-PIN שלך עם הסיסמה שלך", "reset_sqlite": "אפס את מסד הנתונים SQLite", "reset_sqlite_confirmation": "האם אתה בטוח שברצונך לאפס את מסד הנתונים SQLite? יהיה עליך להתנתק ולהתחבר מחדש כדי לסנכרן את הנתונים מחדש", "reset_sqlite_success": "איפוס מסד הנתונים SQLite בוצע בהצלחה", @@ -1600,6 +1670,7 @@ "restore_user": "שחזר משתמש", "restored_asset": "התמונה שוחזרה", "resume": "המשך", + "resume_paused_jobs": "המשך {count, plural, one {עבודה # שהופסקה} other {# עבודות שהופסקו}}", "retry_upload": "נסה שוב להעלות", "review_duplicates": "בדוק כפילויות", "review_large_files": "צפייה בקבצים גדולים", @@ -1680,7 +1751,7 @@ "select_all": "בחר הכל", "select_all_duplicates": "בחר את כל הכפילויות", "select_all_in": "בחר הכול בתוך {group}", - "select_avatar_color": "בחר צבע תמונת פרופיל", + "select_avatar_color": "בחר צבע יצגן", "select_face": "בחר פנים", "select_featured_photo": "בחר תמונה מייצגת", "select_from_computer": "בחר מהמחשב", @@ -1693,6 +1764,7 @@ "select_user_for_sharing_page_err_album": "יצירת אלבום נכשלה", "selected": "נבחרו", "selected_count": "{count, plural, other {# נבחרו}}", + "selected_gps_coordinates": "קואורדינטות GPS שנבחרו", "send_message": "שלח הודעה", "send_welcome_email": "שלח דוא\"ל קבלת פנים", "server_endpoint": "נקודת קצה שרת", @@ -1821,6 +1893,7 @@ "show_slideshow_transition": "הצג מעבר מצגת", "show_supporter_badge": "תג תומך", "show_supporter_badge_description": "הצג תג תומך", + "show_text_search_menu": "הצג תפריט חיפוש טקסט", "shuffle": "ערבוב", "sidebar": "סרגל צד", "sidebar_display_description": "הצג קישור לתצוגה בסרגל הצד", @@ -1836,6 +1909,7 @@ "sort_created": "תאריך יצירה", "sort_items": "מספר פריטים", "sort_modified": "תאריך שינוי", + "sort_newest": "תמונה הכי חדשה", "sort_oldest": "תמונה הכי ישנה", "sort_people_by_similarity": "מיין אנשים לפי דמיון", "sort_recent": "תמונה אחרונה ביותר", @@ -1850,6 +1924,7 @@ "stacktrace": "Stack trace", "start": "התחל", "start_date": "תאריך התחלה", + "start_date_before_end_date": "תאריך ההתחלה חייב להיות לפני תאריך הסיום", "state": "מדינה", "status": "מצב", "stop_casting": "הפסקת שידור", @@ -1864,7 +1939,7 @@ "submit": "שלח", "success": "בוצע בהצלחה", "suggestions": "הצעות", - "sunrise_on_the_beach": "Sunrise on the beach (מומלץ לחפש באנגלית לתוצאות טובות יותר)", + "sunrise_on_the_beach": "שקיעה בחוף", "support": "תמיכה", "support_and_feedback": "תמיכה & משוב", "support_third_party_description": "התקנת ה-Immich שלך נארזה על ידי צד שלישי. בעיות שאתה חווה עשויות להיגרם על ידי חבילה זו, אז בבקשה תעלה בעיות איתם ראשית כל באמצעות הקישורים למטה.", @@ -1873,7 +1948,9 @@ "sync_albums": "סנכרן אלבומים", "sync_albums_manual_subtitle": "סנכרן את כל הסרטונים והתמונות שהועלו לאלבומי הגיבוי שנבחרו", "sync_local": "סנכרן מקומי", - "sync_remote": "סנכרן מרוחק", + "sync_remote": "סנכרן נקודת קצה מרוחקת", + "sync_status": "סנכרן מצב", + "sync_status_subtitle": "הצג ונהל את מערכת הסנכרון", "sync_upload_album_setting_subtitle": "צור והעלה תמונות וסרטונים שלך לאלבומים שנבחרו ביישום", "tag": "תג", "tag_assets": "תיוג תמונות", @@ -1911,7 +1988,9 @@ "to_change_password": "שנה סיסמה", "to_favorite": "מועדף", "to_login": "כניסה", + "to_multi_select": "לבחור מרובות", "to_parent": "לך להורה", + "to_select": "לבחור", "to_trash": "אשפה", "toggle_settings": "החלף מצב הגדרות", "total": "סה\"כ", @@ -1931,6 +2010,7 @@ "trash_page_select_assets_btn": "בחר תמונות", "trash_page_title": "אשפה ({count})", "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.", + "troubleshoot": "פתור בעיות", "type": "סוג", "unable_to_change_pin_code": "לא ניתן לשנות את קוד ה PIN", "unable_to_setup_pin_code": "לא ניתן להגדיר קוד PIN", @@ -1961,6 +2041,7 @@ "unstacked_assets_count": "{count, plural, one {תמונה # הוסרה} other {# תמונות הוסרו}} מהערימה", "untagged": "לא מתיוגים", "up_next": "הבא בתור", + "update_location_action_prompt": "עדכן את המיקום של {count} פריטים שנבחרו עם:", "updated_at": "עודכן", "updated_password": "סיסמה עודכנה", "upload": "העלאה", @@ -2027,6 +2108,7 @@ "view_next_asset": "הצג את התמונה הבאה", "view_previous_asset": "הצג את התמונה הקודמת", "view_qr_code": "הצג ברקוד", + "view_similar_photos": "הצג תמונות דומות", "view_stack": "הצג ערימה", "view_user": "הצג משתמש", "viewer_remove_from_stack": "הסר מערימה", @@ -2045,5 +2127,6 @@ "yes": "כן", "you_dont_have_any_shared_links": "אין לך קישורים משותפים", "your_wifi_name": "שם אינטרנט אלחוטי שלך", - "zoom_image": "זום לתמונה" + "zoom_image": "זום לתמונה", + "zoom_to_bounds": "התמקד באזור" } diff --git a/i18n/hi.json b/i18n/hi.json index 9662eb1203..385a85c2e2 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -381,8 +381,6 @@ "admin_password": "व्यवस्थापक पासवर्ड", "administration": "प्रशासन", "advanced": "विकसित", - "advanced_settings_beta_timeline_subtitle": "नए ऐप अनुभव को आज़माएँ", - "advanced_settings_beta_timeline_title": "बीटा टाइमलाइन", "advanced_settings_enable_alternate_media_filter_subtitle": "सिंक के दौरान वैकल्पिक मानदंडों के आधार पर मीडिया को फ़िल्टर करने के लिए इस विकल्प का उपयोग करें। इसे केवल तभी आज़माएँ जब आपको ऐप द्वारा सभी एल्बमों का पता लगाने में समस्या हो।", "advanced_settings_enable_alternate_media_filter_title": "[प्रयोगात्मक] वैकल्पिक डिवाइस एल्बम सिंक फ़िल्टर का उपयोग करें", "advanced_settings_log_level_title": "लॉग स्तर:{level}", @@ -585,8 +583,6 @@ "backup_setting_subtitle": "पृष्ठभूमि और अग्रभूमि अपलोड सेटिंग प्रबंधित करें", "backup_settings_subtitle": "अपलोड सेटिंग्स संभालें", "backward": "पिछला", - "beta_sync": "बीटा सिंक स्थिति", - "beta_sync_subtitle": "नए सिंक सिस्टम का प्रबंधन करें", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण सक्षम", "biometric_locked_out": "आप बायोमेट्रिक प्रमाणीकरण से बाहर हैं", "biometric_no_options": "कोई बायोमेट्रिक विकल्प उपलब्ध नहीं है", @@ -1550,6 +1546,7 @@ "year": "वर्ष", "yes": "हाँ", "you_dont_have_any_shared_links": "आपके पास कोई साझा लिंक नहीं है", - "your_wifi_name": "Your WiFi name", - "zoom_image": "छवि ज़ूम करें" + "your_wifi_name": "आपके वाईफाई का नाम", + "zoom_image": "छवि ज़ूम करें", + "zoom_to_bounds": "सीमा तक ज़ूम करें" } diff --git a/i18n/hr.json b/i18n/hr.json index 036078f23a..32331976c1 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -387,8 +387,6 @@ "admin_password": "Admin lozinka", "administration": "Administracija", "advanced": "Napredno", - "advanced_settings_beta_timeline_subtitle": "Isprobaj novo iskustvo aplikacije", - "advanced_settings_beta_timeline_title": "Beta vremenska crta", "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: {level}", @@ -594,8 +592,6 @@ "backup_setting_subtitle": "Upravljajte postavkama učitavanja u pozadini i prvom planu", "backup_settings_subtitle": "Upravljaj postavkama slanja", "backward": "Unazad", - "beta_sync": "Beta status sinkronizacije", - "beta_sync_subtitle": "Upravljaj novim sustavom sinkronizacije", "biometric_auth_enabled": "Biometrijska autentikacija omogućena", "biometric_locked_out": "Zaključani ste iz biometrijske autentikacije", "biometric_no_options": "Nema dostupnih biometrijskih opcija", diff --git a/i18n/hu.json b/i18n/hu.json index 490833a794..f000e89517 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -123,6 +123,13 @@ "logging_enable_description": "Naplózás engedélyezése", "logging_level_description": "Ha be van kapcsolva, milyen részletességű legyen a naplózás.", "logging_settings": "Naplózás", + "machine_learning_availability_checks": "Elérhetőség ellenőrzése", + "machine_learning_availability_checks_description": "Automatikusan keressen és válasszon elérhető gépi tanulás szervereket", + "machine_learning_availability_checks_enabled": "Elérhetőség ellenőrzésének bekapcsolása", + "machine_learning_availability_checks_interval": "Ellenőrzési intervallum", + "machine_learning_availability_checks_interval_description": "Elérhetőség-ellenőrzések közötti késleltetés milliszekundumban", + "machine_learning_availability_checks_timeout": "Kérések időkorlátja", + "machine_learning_availability_checks_timeout_description": "Elérhetőség-ellenőrzések időkorlátja milliszekundumban", "machine_learning_clip_model": "CLIP modell", "machine_learning_clip_model_description": "Egy CLIP modell neve az itt felsoroltak közül. A modell megváltoztatása után újra kell futtatni az 'Okos Keresés' feladatot minden képre.", "machine_learning_duplicate_detection": "Duplikációk Keresése", @@ -387,8 +394,6 @@ "admin_password": "Admin Jelszó", "administration": "Adminisztráció", "advanced": "Haladó", - "advanced_settings_beta_timeline_subtitle": "Próbáld ki az új alkalmazást", - "advanced_settings_beta_timeline_title": "Béta Idővonal", "advanced_settings_enable_alternate_media_filter_subtitle": "Ezzel a beállítással a szinkronizálás során alternatív kritériumok alapján szűrheted a fájlokat. Csak akkor próbáld ki, ha problémáid vannak azzal, hogy az alkalmazás nem ismeri fel az összes albumot.", "advanced_settings_enable_alternate_media_filter_title": "[KÍSÉRLETI] Alternatív eszköz album szinkronizálási szűrő használata", "advanced_settings_log_level_title": "Naplózás szintje: {level}", @@ -396,6 +401,8 @@ "advanced_settings_prefer_remote_title": "Távoli képek előnyben részesítése", "advanced_settings_proxy_headers_subtitle": "Add meg azokat a proxy fejléceket, amiket az app elküldjön minden hálózati kérésnél", "advanced_settings_proxy_headers_title": "Proxy Fejlécek", + "advanced_settings_readonly_mode_subtitle": "Bekapcsol egy írásvédett módot ahol csak fotókat nézni lehetséges, egyebek, mint több kép kiválasztása, megosztás, kivetítés és törlés ki vannak kapcsolva. Ki/bekapcsolható a felhasználó ikonjáról a fő képernyőn", + "advanced_settings_readonly_mode_title": "Írásvédett Mód", "advanced_settings_self_signed_ssl_subtitle": "Nem ellenőrzi a szerver SSL tanúsítványát. Önaláírt tanúsítvány esetén szükséges beállítás.", "advanced_settings_self_signed_ssl_title": "Önaláírt SSL tanúsítványok engedélyezése", "advanced_settings_sync_remote_deletions_subtitle": "Automatikusan törölni vagy visszaállítani egy elemet ezen az eszközön, ha az adott műveletet a weben hajtották végre", @@ -423,6 +430,7 @@ "album_remove_user_confirmation": "Biztos, hogy el szeretnéd távolítani {user} felhasználót?", "album_search_not_found": "Nem található a keresésnek megfelelő album", "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_summary": "Album összefogalaló", "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", @@ -461,6 +469,7 @@ "app_bar_signout_dialog_title": "Kijelentkezés", "app_settings": "Alkalmazás Beállítások", "appears_in": "Itt szerepel", + "apply_count": "Alkalmaz ({count, number})", "archive": "Archívum", "archive_action_prompt": "{count} elem hozzáadva az Archívumhoz", "archive_or_unarchive_photo": "Fotó archiválása vagy archiválásának visszavonása", @@ -493,6 +502,8 @@ "asset_restored_successfully": "Elem sikeresen helyreállítva", "asset_skipped": "Kihagyva", "asset_skipped_in_trash": "Lomtárban", + "asset_trashed": "Elem lomtárba helyezve", + "asset_troubleshoot": "Hibajavítás", "asset_uploaded": "Feltöltve", "asset_uploading": "Feltöltés…", "asset_viewer_settings_subtitle": "A képnézegető beállításainak kezelése", @@ -500,7 +511,7 @@ "assets": "Elemek", "assets_added_count": "{count, plural, other {# elem}} hozzáadva", "assets_added_to_album_count": "{count, plural, other {# elem}} hozzáadva az albumhoz", - "assets_added_to_albums_count": "Az {assetTotal, plural, one {elem} other {elemek}} hozzáadva {albumTotal} albumhoz", + "assets_added_to_albums_count": "{assetTotal, plural, one {# elem} other {# elemek}} hozzáadva {albumTotal, plural, one {# albumhoz} other {# albumokhoz}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Az elem} other {Az elemek}} nem adhatóak hozzá az albumhoz", "assets_cannot_be_added_to_albums": "Az {count, plural, one {elemet} other {elemeket}} nem lehet hozzáadni egy albumhoz sem", "assets_count": "{count, plural, other {# elem}}", @@ -526,8 +537,10 @@ "autoplay_slideshow": "Automatikus diavetítés", "back": "Vissza", "back_close_deselect": "Vissza, bezárás, vagy kijelölés törlése", + "background_backup_running_error": "Háttérben futó mentés folyamatban, kézi mentés nem indítható", "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", + "background_options": "Háttérbeli futás beállításai", "backup": "Mentés", "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", @@ -535,6 +548,7 @@ "backup_album_selection_page_select_albums": "Válassz albumokat", "backup_album_selection_page_selection_info": "Összegzés", "backup_album_selection_page_total_assets": "Összes egyedi elem", + "backup_albums_sync": "Backup albumok szinkronizálása", "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…", @@ -594,8 +608,6 @@ "backup_setting_subtitle": "A háttérben és előtérben mentés beállításainak kezelése", "backup_settings_subtitle": "Feltöltés beállításai", "backward": "Visszafele", - "beta_sync": "Béta Szinkronizálás Állapota", - "beta_sync_subtitle": "Az új szinkronizálási rendszer kezelése", "biometric_auth_enabled": "Biometrikus azonosítás engedélyezve", "biometric_locked_out": "Ki vagy zárva a biometrikus azonosításból", "biometric_no_options": "Nincsen elérhető biometrikus azonosítás", @@ -653,6 +665,8 @@ "change_pin_code": "PIN kód megváltoztatása", "change_your_password": "Jelszavad megváltoztatása", "changed_visibility_successfully": "Láthatóság sikeresen megváltoztatva", + "charging": "Töltés", + "charging_requirement_mobile_backup": "Háttérben mentéshez szükséges, hogy az eszköz töltőn legyen", "check_corrupt_asset_backup": "Sérült elemek keresése a mentésben", "check_corrupt_asset_backup_button": "Ellenőrzés", "check_corrupt_asset_backup_description": "Ezt az ellenőtzést csak Wi-Fi hálózaton futtasd és csak akkot, ha már az összes elem feltöltésre került. A folyamat néhány percig is eltarthat.", @@ -739,6 +753,7 @@ "create_user": "Felhasználó létrehozása", "created": "Készült", "created_at": "Létrehozva", + "creating_linked_albums": "Kapcsolt albumok létrehozása...", "crop": "Kivágás", "curated_object_page_title": "Dolgok", "current_device": "Ez az eszköz", @@ -888,7 +903,9 @@ "error": "Hiba", "error_change_sort_album": "Album sorbarendezésének megváltoztatása sikertelen", "error_delete_face": "Hiba az arc törlése során", + "error_getting_places": "Hiba a helyek betöltésekor", "error_loading_image": "Hiba a kép betöltése közben", + "error_loading_partners": "Hiba a partnerek betöltésénél: {error}", "error_saving_image": "Hiba: {error}", "error_tag_face_bounding_box": "Hiba az arc megjelölése közben - nem elérhetőek a határoló koordináták", "error_title": "Hiba - valami félresikerült", @@ -1053,6 +1070,7 @@ "favorites_page_no_favorites": "Nem található kedvencnek jelölt elem", "feature_photo_updated": "Címlapkép frissítve", "features": "Jellemzők", + "features_in_development": "Folyamatban lévő fejlesztések", "features_setting_description": "Az alkalmazás jellemzőinek kezelése", "file_name": "Fájlnév", "file_name_or_extension": "Fájlnév vagy kiterjesztés", @@ -1073,12 +1091,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ez a funkció a Google-től tölti be a működéséhez szükséges külső adatokat.", "general": "Általános", + "geolocation_instruction_location": "Kattints egy elemre, amelynek ismert a helyszíne a pozíció kiválasztásához, vagy válassz a térképen", "get_help": "Segítségkérés", "get_wifiname_error": "Nem sikerült lekérni a Wi-Fi nevét. Győződj meg róla, hogy megadtad a szükséges engedélyeket és csatlakoztál egy Wi-Fi hálózathoz", "getting_started": "Kezdő Lépések", "go_back": "Visszalépés", "go_to_folder": "Ugrás a mappához", "go_to_search": "Ugrás a kereséshez", + "gps": "GPS", + "gps_missing": "Nincs GPS", "grant_permission": "Engedély megadása", "group_albums_by": "Albumok csoportosítása...", "group_country": "Csoportosítás ország szerint", @@ -1214,6 +1235,7 @@ "local": "Helyi", "local_asset_cast_failed": "Nem lehet olyan elemet vetíteni, ami nincs a szerverre feltöltve", "local_assets": "Helyi Elemek", + "local_media_summary": "Helyi média összegzés", "local_network": "Helyi hálózat", "local_network_sheet_info": "Az alkalmazés ezen az URL címen fogja elérni a szervert, ha a megadott WiFi hálózathoz van csatlankozva", "location_permission": "Helymeghatározási engedély", @@ -1225,6 +1247,7 @@ "location_picker_longitude_hint": "Ide írd a hosszúsági kört", "lock": "Zárolás", "locked_folder": "Zárolt mappa", + "log_detail_title": "Naplók részletei", "log_out": "Kijelentkezés", "log_out_all_devices": "Kijelentkezés Minden Eszközön", "logged_in_as": "Belépve: {user} néven", @@ -1255,6 +1278,7 @@ "login_password_changed_success": "Jelszó sikeresen módosítva", "logout_all_device_confirmation": "Biztos, hogy minden eszközön ki szeretnél jelentkezni?", "logout_this_device_confirmation": "Biztos, hogy ki szeretnél jelentkezni ezen az eszközön?", + "logs": "Naplók", "longitude": "Hosszúság", "look": "Megjelenítés", "loop_videos": "Videók ismétlése", @@ -1262,6 +1286,7 @@ "main_branch_warning": "Fejlesztői verziót használsz. Javasoljuk a stabil verzió használatát!", "main_menu": "Főmenü", "make": "Gyártó", + "manage_geolocation": "Helyadatok kezelése", "manage_shared_links": "Megosztási linkek kezelése", "manage_sharing_with_partners": "Partnerekkel való megosztás kezelése", "manage_the_app_settings": "Alkalmazás beállításainak kezelése", @@ -1296,6 +1321,7 @@ "mark_as_read": "Megjelölés olvasottként", "marked_all_as_read": "Összes megjelölve olvasottként", "matches": "Azonosak", + "matching_assets": "Kapcsolódó elemek", "media_type": "Médiatípus", "memories": "Emlékek", "memories_all_caught_up": "Naprakész vagy", @@ -1336,6 +1362,7 @@ "name_or_nickname": "Név vagy becenév", "network_requirement_photos_upload": "Mobil adatforgalmat használjon a fényképek biztonsági mentéséhez", "network_requirement_videos_upload": "Mobil adatforgalmat használjon a videók biztonsági mentéséhez", + "network_requirements": "Hálózati követelmények", "network_requirements_updated": "A hálózat megváltozott, a biztonsági mentési sor visszaállítása", "networking_settings": "Hálózat", "networking_subtitle": "Szerver végpont beállítások kezelése", @@ -1346,6 +1373,7 @@ "new_person": "Új személy", "new_pin_code": "Új PIN kód", "new_pin_code_subtitle": "Ez az első alkalom hogy megnyitod a zárolt mappát. Hozz létre egy jelszót a mappa biztonságos eléréséhez", + "new_timeline": "Új idővonal", "new_user_created": "Új felhasználó létrehozva", "new_version_available": "ÚJ VERZIÓ ÉRHETŐ EL", "newest_first": "Legújabb először", @@ -1359,20 +1387,25 @@ "no_assets_message": "KATTINTS AZ ELSŐ FÉNYKÉP FELTÖLTÉSÉHEZ", "no_assets_to_show": "Nincs megjeleníthető elem", "no_cast_devices_found": "Nem található eszköz vetítéshez", + "no_checksum_local": "Nincs elérhető ellenőrzőösszeg - a helyi eszközök nem kérhetők le", + "no_checksum_remote": "Nincs elérhető ellenőrzőösszeg - a távoli eszköz nem kérhető le", "no_duplicates_found": "Nem találhatók duplikátumok.", "no_exif_info_available": "Nincs elérhető Exif információ", "no_explore_results_message": "Tölts fel több képet, hogy böngészhesd a gyűjteményed.", "no_favorites_message": "Add hozzá a kedvencekhez, hogy gyorsan megtaláld a legjobb képeidet és videóidat", "no_libraries_message": "Hozz létre külső képtárat a fényképeid és videóid megtekintéséhez", + "no_local_assets_found": "Nem találhatók helyi eszközök ezzel az ellenőrzőösszeggel", "no_locked_photos_message": "A zárolt mappában elhelyezett fotók és videók rejtettek, és nem jelennek meg a könyvtárad böngészése vagy keresése közben sem.", "no_name": "Nincs Név", "no_notifications": "Nincsenek értesítések", "no_people_found": "Nem található személy", "no_places": "Nincsenek helyek", + "no_remote_assets_found": "Nem találhatók távoli eszközök ezzel az ellenőrzőösszeggel", "no_results": "Nincs találat", "no_results_description": "Próbálkozz szinonimákkal vagy általánosabb kulcsszavakkal", "no_shared_albums_message": "Hozz létre egy új albumot, hogy megoszthasd fényképeid és videóid másokkal", "no_uploads_in_progress": "Nincs folyamatban lévő feltöltés", + "not_available": "N/A", "not_in_any_album": "Nincs albumban", "not_selected": "Nincs kiválasztva", "note_apply_storage_label_to_previously_uploaded assets": "Megjegyzés: a korábban feltöltött elemek Tárhely Címkézéséhez futtasd a(z)", @@ -1407,6 +1440,8 @@ "open_the_search_filters": "Keresési szűrők megnyitása", "options": "Beállítások", "or": "vagy", + "organize_into_albums": "Albumokba rendezés", + "organize_into_albums_description": "Meglévő fotók albumokba helyezése, a jelenlegi szinkronizációs beállítások alapján", "organize_your_library": "Rendszerezd a képtáradat", "original": "eredeti", "other": "Egyéb", @@ -1492,6 +1527,7 @@ "port": "Port", "preferences_settings_subtitle": "Alkalmazásbeállítások kezelése", "preferences_settings_title": "Beállítások", + "preparing": "Előkészítés", "preset": "Sablon", "preview": "Előnézet", "previous": "Előző", @@ -1508,6 +1544,7 @@ "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_readonly_mode": "Csak olvasható mód engedélyezve. A kilépéshez hosszan nyomja meg a felhasználói avatar ikont.", "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", @@ -1546,6 +1583,7 @@ "purchase_server_description_2": "Támogató státusz", "purchase_server_title": "Szerver", "purchase_settings_server_activated": "A szerver termékkulcsot az admin kezeli", + "query_asset_id": "Lekérdezési eszköz azonosítója", "queue_status": "Feldolgozva {count}/{total}", "rating": "Értékelés csillagokkal", "rating_clear": "Értékelés törlése", @@ -1553,6 +1591,9 @@ "rating_description": "Exif értékelés megjelenítése az infópanelen", "reaction_options": "Reakció lehetőségek", "read_changelog": "Változásnapló Elolvasása", + "readonly_mode_disabled": "Csak olvasható mód kikapcsolva", + "readonly_mode_enabled": "Csak olvasható mód bekapcsolva", + "ready_for_upload": "Készen áll a feltöltésre", "reassign": "Hozzárendel", "reassigned_assets_to_existing_person": "{count, plural, other {# elem}} hozzárendelve{name, select, null { egy létező személyhez} other {: {name}}}", "reassigned_assets_to_new_person": "{count, plural, other {# elem}} hozzárendelve egy új személyhez", @@ -1577,6 +1618,7 @@ "regenerating_thumbnails": "Bélyegképek újragenerálása folyamatban", "remote": "Távoli", "remote_assets": "Távoli Elemek", + "remote_media_summary": "Távoli médiaösszefoglaló", "remove": "Eltávolítás", "remove_assets_album_confirmation": "Biztosan el szeretnél távolítani {count, plural, one {# elemet} other {# elemet}} az albumból?", "remove_assets_shared_link_confirmation": "Biztosan el szeretnél távolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?", @@ -1629,6 +1671,7 @@ "restore_user": "Felhasználó visszaállítása", "restored_asset": "Visszaállított elem", "resume": "Folytatás", + "resume_paused_jobs": "Folytatás {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Feltöltés újrapróbálása", "review_duplicates": "Duplikátumok áttekintése", "review_large_files": "Nagy fájlok áttekintése", @@ -1722,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Az album létrehozása sikertelen", "selected": "Kiválasztott", "selected_count": "{count, plural, other {# kiválasztva}}", + "selected_gps_coordinates": "Kiválasztott GPS Kordináták", "send_message": "Üzenet küldése", "send_welcome_email": "Üdvözlő email küldése", "server_endpoint": "Szerver Végpont", @@ -1850,6 +1894,7 @@ "show_slideshow_transition": "Vetítés áttűnési effekt mutatása", "show_supporter_badge": "Támogató jelvény", "show_supporter_badge_description": "Támogató jelvény mutatása", + "show_text_search_menu": "Mutasd a szövegkeresési menüt", "shuffle": "Véletlenszerű", "sidebar": "Oldalsáv", "sidebar_display_description": "Nézet link megjelenítése az oldalsávban", @@ -1880,6 +1925,7 @@ "stacktrace": "Hiba leírása", "start": "Elindít", "start_date": "Kezdő dátum", + "start_date_before_end_date": "A kezdeti dátumnak a befejezési dátum előtt kell lennie", "state": "Megye/Állam", "status": "Állapot", "stop_casting": "Vetítés megszüntetése", @@ -1904,6 +1950,8 @@ "sync_albums_manual_subtitle": "Összes fotó és videó létrehozása és szinkronizálása a kiválasztott Immich albumokba", "sync_local": "Helyi Szinkronizálása", "sync_remote": "Távoli Szinkronizálása", + "sync_status": "Szinkronizálás állapota", + "sync_status_subtitle": "Szinkronizálás megtekintése és kezelése", "sync_upload_album_setting_subtitle": "Fotók és videók létrehozása és szinkronizálása a kiválasztott Immich albumba", "tag": "Címke", "tag_assets": "Elemek címkézése", @@ -1941,7 +1989,9 @@ "to_change_password": "Jelszó megváltoztatása", "to_favorite": "Kedvenc", "to_login": "Bejelentkezés", + "to_multi_select": "több elem kiválasztásához", "to_parent": "Egy szinttel feljebb", + "to_select": "a kiválasztáshoz", "to_trash": "Lomtárba helyezés", "toggle_settings": "Beállítások átállítása", "total": "Összesen", @@ -1961,6 +2011,7 @@ "trash_page_select_assets_btn": "Elemek kiválasztása", "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.", + "troubleshoot": "Hibaelhárítás", "type": "Típus", "unable_to_change_pin_code": "Sikertelen PIN kód változtatás", "unable_to_setup_pin_code": "Sikertelen PIN kód beállítás", @@ -1991,6 +2042,7 @@ "unstacked_assets_count": "{count, plural, other {# elemből}} álló csoport szétszedve", "untagged": "Címke eltávolítva", "up_next": "Következik", + "update_location_action_prompt": "{count} elem pozíciójának frissítése a következővel:", "updated_at": "Frissített", "updated_password": "Jelszó megváltoztatva", "upload": "Feltöltés", @@ -2057,6 +2109,7 @@ "view_next_asset": "Következő elem megtekintése", "view_previous_asset": "Előző elem megtekintése", "view_qr_code": "QR kód megtekintése", + "view_similar_photos": "Hasonló képek keresése", "view_stack": "Csoport Megtekintése", "view_user": "Felhasználó Megtekintése", "viewer_remove_from_stack": "Eltávolít a Csoportból", @@ -2075,5 +2128,6 @@ "yes": "Igen", "you_dont_have_any_shared_links": "Nincsenek megosztott linkjeid", "your_wifi_name": "A Wi-Fi hálózatod neve", - "zoom_image": "Kép Nagyítása" + "zoom_image": "Kép Nagyítása", + "zoom_to_bounds": "Nagyítás a határokhoz" } diff --git a/i18n/id.json b/i18n/id.json index f8de73b3be..8dba86752e 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -28,6 +28,9 @@ "add_to_album": "Tambahkan ke album", "add_to_album_bottom_sheet_added": "Ditambahkan ke {album}", "add_to_album_bottom_sheet_already_exists": "Sudah ada di {album}", + "add_to_album_toggle": "Masukkan ke {album} / Batalkan dari {album}", + "add_to_albums": "Tambahkan ke album", + "add_to_albums_count": "Tambahkan ke album ({count})", "add_to_shared_album": "Tambahkan ke album terbagi", "add_url": "Tambahkan URL", "added_to_archive": "Ditambahkan ke arsip", @@ -120,6 +123,13 @@ "logging_enable_description": "Aktifkan log", "logging_level_description": "Ketika diaktifkan, tingkat log apa yang digunakan.", "logging_settings": "Penulisan log", + "machine_learning_availability_checks": "Pemeriksaan ketersediaan", + "machine_learning_availability_checks_description": "Secara otomatis mendeteksi dan memprioritaskan server machine learning yang tersedia", + "machine_learning_availability_checks_enabled": "Aktifkan pemeriksaan ketersediaan", + "machine_learning_availability_checks_interval": "Interval pemeriksaan", + "machine_learning_availability_checks_interval_description": "Interval dalam milidetik antar pemeriksaan ketersediaan", + "machine_learning_availability_checks_timeout": "Batas waktu permintaan", + "machine_learning_availability_checks_timeout_description": "Batas waktu dalam milidetik untuk pemeriksaan ketersediaan", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "Nama model CLIP yang didaftarkan di sini. Anda harus menjalankan ulang tugas 'Pencarian Otomatis' untuk semua gambar ketika mengganti model.", "machine_learning_duplicate_detection": "Deteksi Duplikat", @@ -384,8 +394,6 @@ "admin_password": "Kata Sandi Admin", "administration": "Administrasi", "advanced": "Tingkat lanjut", - "advanced_settings_beta_timeline_subtitle": "Coba pengalaman aplikasi baru", - "advanced_settings_beta_timeline_title": "Garis waktu Beta", "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: {level}", @@ -393,6 +401,8 @@ "advanced_settings_prefer_remote_title": "Prioritaskan gambar dari server", "advanced_settings_proxy_headers_subtitle": "Tentukan header proxy yang harus dikirim Immich dengan setiap permintaan jaringan", "advanced_settings_proxy_headers_title": "Tajuk Proksi", + "advanced_settings_readonly_mode_subtitle": "Mengaktifkan mode baca-saja, di mana foto hanya bisa dilihat. Fitur seperti memilih banyak foto, berbagi, cast, dan hapus akan dinonaktifkan. Mode baca-saja bisa diaktifkan/nonaktifkan lewat avatar pengguna di layar utama", + "advanced_settings_readonly_mode_title": "Mode Baca-Saja", "advanced_settings_self_signed_ssl_subtitle": "Melewati verifikasi sertifikat SSL untuk titik akhir server. Diperlukan untuk sertifikat yang ditandatangani sendiri.", "advanced_settings_self_signed_ssl_title": "Izinkan sertifikat SSL yang ditandatangani sendiri", "advanced_settings_sync_remote_deletions_subtitle": "Hapus atau pulihkan aset pada perangkat ini secara otomatis ketika tindakan dilakukan di web", @@ -420,6 +430,7 @@ "album_remove_user_confirmation": "Apakah Anda yakin ingin mengeluarkan {user}?", "album_search_not_found": "Tidak ada album yang ditemukan sesuai pencarian Anda", "album_share_no_users": "Sepertinya Anda telah membagikan album ini dengan semua pengguna atau tidak memiliki pengguna siapa pun untuk dibagikan.", + "album_summary": "Ringkasan album", "album_updated": "Album diperbarui", "album_updated_setting_description": "Terima notifikasi surel ketika album terbagi memiliki aset baru", "album_user_left": "Keluar dari {album}", @@ -458,6 +469,7 @@ "app_bar_signout_dialog_title": "Keluar akun", "app_settings": "Pengaturan Aplikasi", "appears_in": "Muncul dalam", + "apply_count": "Terapkan ({count, number})", "archive": "Arsip", "archive_action_prompt": "{count} telah ditambahkan ke Arsip", "archive_or_unarchive_photo": "Arsipkan atau batalkan pengarsipan foto", @@ -490,6 +502,8 @@ "asset_restored_successfully": "Aset telah berhasil dipulihkan", "asset_skipped": "Dilewati", "asset_skipped_in_trash": "Dalam sampah", + "asset_trashed": "Aset dibuang", + "asset_troubleshoot": "Troubleshoot Aset", "asset_uploaded": "Sudah diunggah", "asset_uploading": "Mengunggah…", "asset_viewer_settings_subtitle": "Kelola pengaturan penampil galeri Anda", @@ -497,7 +511,9 @@ "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_albums_count": "Ditambahkan {assetTotal, plural, one {# aset} other {# aset}} ke {albumTotal, plural, one {# album} other {# album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} tidak dapat ditambahkan ke album", + "assets_cannot_be_added_to_albums": "{count, plural, one {Aset} other {Aset}} tidak dapat ditambahkan ke album mana pun", "assets_count": "{count, plural, one {# aset} other {# aset}}", "assets_deleted_permanently": "{count} aset dihapus secara permanen", "assets_deleted_permanently_from_server": "{count} aset dihapus secara permanen dari server Immich", @@ -514,14 +530,17 @@ "assets_trashed_count": "{count, plural, one {# aset} other {# aset}} dibuang ke sampah", "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", + "assets_were_part_of_albums_count": "{count, plural, one {Aset sudah} other {Aset sudah}} ada di album", "authorized_devices": "Perangkat Terautentikasi", "automatic_endpoint_switching_subtitle": "Sambungkan secara lokal melalui Wi-Fi yang telah ditetapkan saat tersedia, dan gunakan koneksi alternatif lain", "automatic_endpoint_switching_title": "Peralihan URL otomatis", "autoplay_slideshow": "Putar otomatis tayangan slide", "back": "Kembali", "back_close_deselect": "Kembali, tutup, atau batalkan pemilihan", + "background_backup_running_error": "Cadangan latar belakang sedang berjalan, tidak dapat memulai cadangan manual", "background_location_permission": "Izin lokasi latar belakang", "background_location_permission_content": "Untuk beralih jaringan saat berjalan di latar belakang, Immich harus selalu memiliki akses lokasi akurat agar aplikasi dapat membaca nama jaringan Wi-Fi", + "background_options": "Opsi Latar Belakang", "backup": "Cadangkan", "backup_album_selection_page_albums_device": "Album di perangkat ({count})", "backup_album_selection_page_albums_tap": "Sentuh untuk memilih, sentuh 2x untuk mengecualikan", @@ -529,6 +548,7 @@ "backup_album_selection_page_select_albums": "Pilih album", "backup_album_selection_page_selection_info": "Info Pilihan", "backup_album_selection_page_total_assets": "Total aset unik", + "backup_albums_sync": "Sinkronisasi cadangan album", "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…", @@ -588,8 +608,6 @@ "backup_setting_subtitle": "Kelola pengaturan unggahan latar belakang dan latar depan", "backup_settings_subtitle": "Kelola pengaturan unggahan", "backward": "Maju", - "beta_sync": "Status proses sinkronisasi versi beta", - "beta_sync_subtitle": "Kelola sistem sinkronisasi baru", "biometric_auth_enabled": "Autentikasi biometrik diaktifkan", "biometric_locked_out": "Anda terkunci oleh autentikasi biometrik", "biometric_no_options": "Opsi biometrik tidak tersedia", @@ -647,6 +665,8 @@ "change_pin_code": "Ubah kode PIN", "change_your_password": "Ubah kata sandi Anda", "changed_visibility_successfully": "Keterlihatan berhasil diubah", + "charging": "Mengisi daya", + "charging_requirement_mobile_backup": "Cadangan latar belakang memerlukan perangkat dalam keadaan mengisi daya", "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.", @@ -733,6 +753,7 @@ "create_user": "Buat pengguna", "created": "Dibuat", "created_at": "Dibuat", + "creating_linked_albums": "Membuat album tertaut...", "crop": "Pangkas", "curated_object_page_title": "Benda", "current_device": "Perangkat saat ini", @@ -882,7 +903,9 @@ "error": "Eror", "error_change_sort_album": "Gagal mengubah urutan album", "error_delete_face": "Terjadi kesalahan menghapus wajah dari aset", + "error_getting_places": "Kesalahan saat mengambil lokasi", "error_loading_image": "Terjadi eror memuat gambar", + "error_loading_partners": "Kesalahan saat memuat partner: {error}", "error_saving_image": "Kesalahan: {error}", "error_tag_face_bounding_box": "Galat saat memberi tag wajah – tidak dapat memperoleh koordinat kotak pembatas", "error_title": "Eror - Ada yang salah", @@ -1047,6 +1070,7 @@ "favorites_page_no_favorites": "Tidak ada aset favorit", "feature_photo_updated": "Foto terfitur diperbarui", "features": "Fitur", + "features_in_development": "Fitur dalam Pengembangan", "features_setting_description": "Kelola fitur aplikasi", "file_name": "Nama berkas", "file_name_or_extension": "Nama berkas atau ekstensi", @@ -1056,6 +1080,7 @@ "filter_people": "Saring orang", "filter_places": "Saring tempat", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", + "first": "Pertama", "fix_incorrect_match": "Perbaiki pencocokan salah", "folder": "Berkas", "folder_not_found": "Berkas tidak ditemukan", @@ -1066,12 +1091,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Fitur ini memuat sumber daya eksternal dari Google agar dapat berfungsi.", "general": "Umum", + "geolocation_instruction_location": "Klik aset yang memiliki koordinat GPS untuk menggunakan lokasinya, atau pilih lokasi langsung dari peta", "get_help": "Dapatkan Bantuan", "get_wifiname_error": "Tidak dapat mendapatkan nama Wi-Fi. Pastikan Anda telah memberikan izin yang diperlukan dan terhubung ke jaringan Wi-Fi", "getting_started": "Memulai", "go_back": "Kembali", "go_to_folder": "Pergi ke folder", "go_to_search": "Pergi ke pencarian", + "gps": "GPS", + "gps_missing": "Tidak ada GPS", "grant_permission": "Izinkan", "group_albums_by": "Kelompokkan album berdasarkan...", "group_country": "Kelompokkan berdasarkan negara", @@ -1177,6 +1205,7 @@ "language_search_hint": "Mencari Bahasa...", "language_setting_description": "Pilih bahasa Anda yang disukai", "large_files": "File Besar", + "last": "Terakhir", "last_seen": "Terakhir dilihat", "latest_version": "Versi Terkini", "latitude": "Lintang", @@ -1195,6 +1224,7 @@ "library_page_sort_title": "Judul album", "licenses": "Lisensi", "light": "Terang", + "like": "Suka", "like_deleted": "Suka dihapus", "link_motion_video": "Tautan video gerak", "link_to_oauth": "Tautkan ke OAuth", @@ -1205,6 +1235,7 @@ "local": "Lokal", "local_asset_cast_failed": "Tidak dapat melakukan cast aset yang belum diunggah ke server", "local_assets": "Aset Lokal", + "local_media_summary": "Ringkasan Media Lokal", "local_network": "Jaringan Lokal", "local_network_sheet_info": "Aplikasi akan terhubung ke server melalui URL ini saat menggunakan jaringan Wi-Fi yang ditentukan", "location_permission": "Izin lokasi", @@ -1216,6 +1247,7 @@ "location_picker_longitude_hint": "Masukkan bujur di sini", "lock": "Kunci", "locked_folder": "Folder Terkunci", + "log_detail_title": "Detail Log", "log_out": "Log keluar", "log_out_all_devices": "Keluar dari Semua Perangkat", "logged_in_as": "Masuk sebagai {user}", @@ -1246,6 +1278,7 @@ "login_password_changed_success": "Sandi berhasil diperbarui", "logout_all_device_confirmation": "Apakah Anda yakin ingin keluar dari semua perangkat?", "logout_this_device_confirmation": "Apakah Anda yakin ingin mengeluarkan perangkat ini?", + "logs": "Log", "longitude": "Bujur", "look": "Tampilan", "loop_videos": "Ulangi video", @@ -1253,6 +1286,7 @@ "main_branch_warning": "Anda menggunakan versi pengembangan; kami sangat menyarankan menggunakan versi rilis!", "main_menu": "Menu utama", "make": "Merek", + "manage_geolocation": "Atur lokasi", "manage_shared_links": "Kelola tautan terbagi", "manage_sharing_with_partners": "Kelola pembagian dengan partner", "manage_the_app_settings": "Kelola pengaturan aplikasi", @@ -1287,6 +1321,7 @@ "mark_as_read": "Tandai sebagai telah dibaca", "marked_all_as_read": "Semua telah ditandai sebagai telah dibaca", "matches": "Cocokan", + "matching_assets": "Aset yang Cocok", "media_type": "Jenis media", "memories": "Kenangan", "memories_all_caught_up": "Semua telah dilihat", @@ -1327,6 +1362,7 @@ "name_or_nickname": "Nama atau nama panggilan", "network_requirement_photos_upload": "Gunakan data seluler untuk cadangkan foto", "network_requirement_videos_upload": "Gunakan data seluler untuk cadangkan video", + "network_requirements": "Persyaratan Jaringan", "network_requirements_updated": "Persyaratan jaringan telah berubah, antrean pencadangan diatur ulang", "networking_settings": "Jaringan", "networking_subtitle": "Kelola pengaturan Endpoint server", @@ -1337,6 +1373,7 @@ "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_timeline": "Linimasa Baru", "new_user_created": "Pengguna baru dibuat", "new_version_available": "VERSI BARU TERSEDIA", "newest_first": "Terkini dahulu", @@ -1350,20 +1387,25 @@ "no_assets_message": "KLIK UNTUK MENGUNGGAH FOTO PERTAMA ANDA", "no_assets_to_show": "Tidak ada aset", "no_cast_devices_found": "Tidak ada perangkat cast yang ditemukan", + "no_checksum_local": "Tidak ada checksum yang tersedia - tidak dapat mengambil aset lokal", + "no_checksum_remote": "Tidak ada checksum yang tersedia - tidak dapat mengambil aset jarak jauh", "no_duplicates_found": "Tidak ada duplikat yang ditemukan.", "no_exif_info_available": "Tidak ada info EXIF yang tersedia", "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_local_assets_found": "Tidak ada aset lokal yang ditemukan dengan checksum ini", "no_locked_photos_message": "Foto dan video di folder terkunci disembunyikan dan tidak akan muncul saat Anda menelusuri atau mencari di pustaka.", "no_name": "Tidak Ada Nama", "no_notifications": "Tidak ada notifikasi", "no_people_found": "Orang tidak ditemukan", "no_places": "Tidak ada tempat", + "no_remote_assets_found": "Tidak ada aset jarak jauh yang ditemukan dengan checksum ini", "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", "no_uploads_in_progress": "Tidak ada unggahan yang sedang berlangsung", + "not_available": "T/T", "not_in_any_album": "Tidak ada dalam album apa pun", "not_selected": "Belum dipilih", "note_apply_storage_label_to_previously_uploaded assets": "Catatan: Untuk menerapkan Label Penyimpanan pada aset yang sebelumnya telah diunggah, jalankan", @@ -1398,6 +1440,8 @@ "open_the_search_filters": "Buka saringan pencarian", "options": "Opsi", "or": "atau", + "organize_into_albums": "Atur ke dalam album", + "organize_into_albums_description": "Masukkan foto lama ke album sesuai pengaturan sinkronisasi", "organize_your_library": "Kelola pustaka Anda", "original": "asli", "other": "Lainnya", @@ -1457,9 +1501,9 @@ "permission_onboarding_permission_limited": "Izin dibatasi. Agai Immich dapat mencadangkan dan mengatur seluruh koleksi galeri, izinkan akses foto dan video pada Setelan.", "permission_onboarding_request": "Immich memerlukan izin untuk melihat foto dan video kamu.", "person": "Orang", - "person_age_months": "{months} bulan", - "person_age_year_months": "1 tahun, {months} bulan", - "person_age_years": "{years} tahun", + "person_age_months": "{months, plural, one {# bulan} other {# bulan}} old", + "person_age_year_months": "1 year, {months, plural, one {# bulan} other {# bulan}} old", + "person_age_years": "{years, plural, other {# tahun}} old", "person_birthdate": "Lahir pada {date}", "person_hidden": "{name}{hidden, select, true { (tersembunyi)} other {}}", "photo_shared_all_users": "Sepertinya Anda membagikan foto Anda dengan semua pengguna atau Anda tidak memiliki pengguna siapa pun untuk dibagikan.", @@ -1483,6 +1527,7 @@ "port": "Porta", "preferences_settings_subtitle": "Kelola preferensi aplikasi", "preferences_settings_title": "Preferensi", + "preparing": "Mempersiapkan", "preset": "Prasetel", "preview": "Pratinjau", "previous": "Sebelumnya", @@ -1499,6 +1544,7 @@ "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_readonly_mode": "Mode baca-saja aktif. Tekan lama ikon avatar pengguna untuk keluar.", "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}", @@ -1537,6 +1583,7 @@ "purchase_server_description_2": "Status pendukung", "purchase_server_title": "Server", "purchase_settings_server_activated": "Kunci produk server dikelola oleh admin", + "query_asset_id": "ID Aset Kueri", "queue_status": "Antrian {count}/{total}", "rating": "Peringkat bintang", "rating_clear": "Hapus peringkat", @@ -1544,6 +1591,9 @@ "rating_description": "Tampilkan peringkat EXIF pada panel info", "reaction_options": "Opsi reaksi", "read_changelog": "Baca Log Perubahan", + "readonly_mode_disabled": "Mode baca-saja dimatikan", + "readonly_mode_enabled": "Mode baca-saja diaktifkan", + "ready_for_upload": "Siap untuk mengunggah", "reassign": "Tetapkan ulang", "reassigned_assets_to_existing_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada {name, select, null {orang yang sudah ada} other {{name}}}", "reassigned_assets_to_new_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada orang baru", @@ -1568,6 +1618,7 @@ "regenerating_thumbnails": "Membuat ulang gambar kecil", "remote": "Jarak Jauh", "remote_assets": "Aset Jarak Jauh", + "remote_media_summary": "Ringkasan Media Jarak Jauh", "remove": "Hapus", "remove_assets_album_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset} other {# aset}} dari album?", "remove_assets_shared_link_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset} other {# aset}} dari tautan terbagi ini?", @@ -1620,6 +1671,7 @@ "restore_user": "Pulihkan pengguna", "restored_asset": "Aset dipulihkan", "resume": "Lanjutkan", + "resume_paused_jobs": "Lanjutkan {count, plural, one {# pekerjaan yang dijeda} other {# pekerjaan yang dijeda}}", "retry_upload": "Ulangi pengunggahan", "review_duplicates": "Pratinjau duplikat", "review_large_files": "Meninjau berkas berukuran besar", @@ -1713,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Gagal membuat album", "selected": "Dipilih", "selected_count": "{count, plural, other {# dipilih}}", + "selected_gps_coordinates": "Koordinat GPS yang dipilih", "send_message": "Kirim pesan", "send_welcome_email": "Kirim surel selamat datang", "server_endpoint": "Endpoint server", @@ -1841,6 +1894,7 @@ "show_slideshow_transition": "Tampilkan transisi salindia", "show_supporter_badge": "Lencana suporter", "show_supporter_badge_description": "Tampilkan lencana suporter", + "show_text_search_menu": "Tampilkan menu pencarian teks", "shuffle": "Acak", "sidebar": "Bilah sisi", "sidebar_display_description": "Menampilkan tautan ke tampilan di bilah sisi", @@ -1856,6 +1910,7 @@ "sort_created": "Tanggal dibuat", "sort_items": "Jumlah item", "sort_modified": "Tanggal diubah", + "sort_newest": "Foto terbaru", "sort_oldest": "Foto terlawas", "sort_people_by_similarity": "Urutkan orang berdasarkan kemiripan", "sort_recent": "Foto paling terkini", @@ -1870,6 +1925,7 @@ "stacktrace": "Jejak tumpukan", "start": "Mulai", "start_date": "Tanggal mulai", + "start_date_before_end_date": "Tanggal mulai harus sebelum tanggal akhir", "state": "Keadaan", "status": "Status", "stop_casting": "Hentikan cast", @@ -1894,6 +1950,8 @@ "sync_albums_manual_subtitle": "Melakukan sinkronisasi semua video dan foto yang telah diunggah ke album cadangan yang dipilih", "sync_local": "Sinkronkan lokal", "sync_remote": "Sinkronkan jarak jauh", + "sync_status": "Status Sinkronisasi", + "sync_status_subtitle": "Lihat dan atur sistem sinkronisasi", "sync_upload_album_setting_subtitle": "Membuat dan mengunggah foto serta video Anda ke album yang telah dipilih pada Immich", "tag": "Label", "tag_assets": "Tag aset", @@ -1931,7 +1989,9 @@ "to_change_password": "Ubah kata sandi", "to_favorite": "Favorit", "to_login": "Log masuk", + "to_multi_select": "untuk memilih beberapa", "to_parent": "Ke induk", + "to_select": "untuk memilih", "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", "total": "Jumlah", @@ -1951,6 +2011,7 @@ "trash_page_select_assets_btn": "Pilih aset", "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}}.", + "troubleshoot": "Pemecahan Masalah", "type": "Jenis", "unable_to_change_pin_code": "Tidak dapat mengubah kode PIN", "unable_to_setup_pin_code": "Tidak dapat memasang kode PIN", @@ -1981,6 +2042,7 @@ "unstacked_assets_count": "Penumpukan {count, plural, one {# aset} other {# aset}} dibatalkan", "untagged": "Tidak ditandai", "up_next": "Berikutnya", + "update_location_action_prompt": "Perbarui lokasi {count} aset yang dipilih dengan:", "updated_at": "Diperbarui", "updated_password": "Kata sandi diperbarui", "upload": "Unggah", @@ -2047,6 +2109,7 @@ "view_next_asset": "Tampilkan aset berikutnya", "view_previous_asset": "Tampilkan aset sebelumnya", "view_qr_code": "Tampilkan kode QR", + "view_similar_photos": "Lihat foto yang mirip", "view_stack": "Tampilkan Tumpukan", "view_user": "Lihat Pengguna", "viewer_remove_from_stack": "Keluarkan dari Tumpukan", @@ -2065,5 +2128,6 @@ "yes": "Ya", "you_dont_have_any_shared_links": "Anda tidak memiliki tautan terbagi", "your_wifi_name": "Nama Wi-Fi Anda", - "zoom_image": "Perbesar Gambar" + "zoom_image": "Perbesar Gambar", + "zoom_to_bounds": "Perbesar ke batas" } diff --git a/i18n/it.json b/i18n/it.json index 7975933160..a86dd78ca6 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -28,6 +28,7 @@ "add_to_album": "Aggiungi all'album", "add_to_album_bottom_sheet_added": "Aggiunto in {album}", "add_to_album_bottom_sheet_already_exists": "Già presente in {album}", + "add_to_album_bottom_sheet_some_local_assets": "Alcune risorse locali non possono essere aggiunte all'album", "add_to_album_toggle": "Attiva/disattiva selezione per {album}", "add_to_albums": "Aggiungi ad album", "add_to_albums_count": "Aggiungi ad album ({count})", @@ -52,7 +53,7 @@ "backup_onboarding_2_description": "copie locali su diversi dispositivi. Ciò include i file principali e un backup di tali file a livello locale.", "backup_onboarding_3_description": "copie totali dei tuoi dati, compresi i file originali. Ciò include 1 copia offsite e 2 copie locali.", "backup_onboarding_description": "Per proteggere i tuoi dati, è consigliato adottare una strategia di backup 3-2-1. Per una soluzione di backup completa, è consigliato conservare copie delle foto/video caricati e del database Immich.", - "backup_onboarding_footer": "Per ulteriori informazioni sul backup di Immich, consultare la documentazione.", + "backup_onboarding_footer": "Per ulteriori informazioni sul backup di Immich, consulta la documentazione.", "backup_onboarding_parts_title": "Un backup 3-2-1 include:", "backup_onboarding_title": "Backup", "backup_settings": "Impostazioni Dump database", @@ -62,7 +63,7 @@ "confirm_delete_library": "Sei sicuro di voler cancellare la libreria {library}?", "confirm_delete_library_assets": "Sei sicuro di voler cancellare questa libreria? Questo cancellerà {count, plural, one {# asset} other {tutti e # gli assets}} da Immich senza possibilità di tornare indietro. I file non verranno cancellati.", "confirm_email_below": "Per confermare, scrivi \"{email}\" qui sotto", - "confirm_reprocess_all_faces": "Sei sicuro di voler riprocessare tutti i volti? Questo cancellerà tutte le persone nominate.", + "confirm_reprocess_all_faces": "Sei sicuro di voler riprocessare tutti i volti? Questo cancellerà anche tutte le persone associate.", "confirm_user_password_reset": "Sei sicuro di voler resettare la password di {user}?", "confirm_user_pin_code_reset": "Sicuro di voler resettare il codice PIN di {user}?", "create_job": "Crea Processo", @@ -79,27 +80,27 @@ "failed_job_command": "Il comando {command} è fallito per il processo: {job}", "force_delete_user_warning": "ATTENZIONE: Questo rimuoverà immediatamente l'utente e tutti i suoi assets. Non è possibile tornare indietro e i file non potranno essere recuperati.", "image_format": "Formato", - "image_format_description": "WebP produce file più piccoli rispetto a JPEG, ma l'encoding è più lento.", - "image_fullsize_description": "Le immagini con dimensioni reali senza metadati sono utilizzate durante lo zoom", - "image_fullsize_enabled": "Abilita la generazione delle immagini con dimensioni reali", - "image_fullsize_enabled_description": "Genera immagini con dimensioni reali per i formati non web-friendly. Quando \"Preferisci l'anteprima integrata\" è abilitata, le anteprime integrate saranno usate senza conversione. Non riguarda le immagini web-friendly come il JPEG.", - "image_fullsize_quality_description": "Qualità delle immagini con dimensioni reali da 1 a 100. Più è alto il valore più la qualità sarà alta come anche la grandezza dei file.", - "image_fullsize_title": "Impostazioni Immagini con dimensioni reali", + "image_format_description": "WebP produce file più piccoli rispetto a JPEG, ma è più lento da codificare.", + "image_fullsize_description": "Immagini a dimensioni reali senza metadati, sono utilizzate durante lo zoom", + "image_fullsize_enabled": "Abilita la generazione delle immagini a dimensioni reali", + "image_fullsize_enabled_description": "Genera immagini a dimensioni reali per i formati non web-friendly. Quando è abilitata l'opzione \"Preferisci l'anteprima integrata\", le anteprime integrate saranno utilizzate direttamente senza conversione. Non influisce sui formati web-friendly come JPEG.", + "image_fullsize_quality_description": "Qualità delle immagini a dimensioni reali da 1 a 100. Un valore più alto è migliore ma produce file più grandi.", + "image_fullsize_title": "Impostazioni delle immagini a dimensioni reali", "image_prefer_embedded_preview": "Preferisci l'anteprima integrata", "image_prefer_embedded_preview_setting_description": "Usa l'anteprima integrata nelle foto RAW come input per l'elaborazione delle immagini, se disponibile. Questo permette un miglioramento dei colori per alcune immagini, ma la qualità delle anteprime dipende dalla macchina fotografica. Inoltre le immagini potrebbero presentare artefatti di compressione.", "image_prefer_wide_gamut": "Preferisci gamut più ampio", "image_prefer_wide_gamut_setting_description": "Usa lo spazio colore Display P3 per le anteprime. Questo aiuta a mantenere la vivacità delle immagini con spazi colore più ampi, tuttavia potrebbe non mostrare correttamente le immagini con dispositivi e browser obsoleti. Le immagini sRGB vengono preservate per evitare alterazioni del colore.", - "image_preview_description": "Immagine di medie dimensioni con metadati eliminati, utilizzata durante la visualizzazione di una singola risorsa e per l'apprendimento automatico", - "image_preview_quality_description": "Qualità dell'anteprima da 1 a 100. Elevata è migliore ma produce file più pesanti e può ridurre la reattività dell'app. Impostare un valore basso può influenzare negativamente la qualità del machine learning.", + "image_preview_description": "Immagine a media dimensione senza metadati, utilizzata durante la visualizzazione di una singola risorsa e per il machine learning", + "image_preview_quality_description": "Qualità dell'anteprima da 1 a 100. Più alto è meglio ma produce file più pesanti e può ridurre la reattività dell'app. Impostare un valore basso può influenzare negativamente la qualità del machine learning.", "image_preview_title": "Impostazioni dell'anteprima", "image_quality": "Qualità", "image_resolution": "Risoluzione", - "image_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedere più tempo per la codifica, avere dimensioni di file più grandi e possono ridurre la reattività dell'app.", + "image_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedere più tempo per la codifica, avere dimensioni di file più grandi e ridurre la reattività dell'app.", "image_settings": "Impostazioni delle immagini", "image_settings_description": "Gestisci qualità e risoluzione delle immagini generate", - "image_thumbnail_description": "Miniatura piccola senza metadati, utilizzata durante la visualizzazione di gruppi di foto come la sequenza temporale principale", - "image_thumbnail_quality_description": "Qualità delle anteprime da 1 a 100. Un valore più alto è migliore ma produce file più grandi e può ridurre la reattività dell'app.", - "image_thumbnail_title": "Impostazioni della copertina", + "image_thumbnail_description": "Miniatura piccola senza metadati, utilizzata durante la visualizzazione di gruppi di foto come nella galleria principale", + "image_thumbnail_quality_description": "Qualità delle miniature da 1 a 100. Un valore più alto è migliore ma produce file più grandi e può ridurre la reattività dell'app.", + "image_thumbnail_title": "Impostazioni delle miniature", "job_concurrency": "Concorrenza {job}", "job_created": "Processo creato", "job_not_concurrency_safe": "Questo processo non è eseguibile in maniera concorrente.", @@ -123,6 +124,13 @@ "logging_enable_description": "Attiva il logging", "logging_level_description": "Quando attivato, che livello di log utilizzare.", "logging_settings": "Registro dei Log", + "machine_learning_availability_checks": "Verifiche di disponibilità", + "machine_learning_availability_checks_description": "Rileva automaticamente e usa i server di machine learning disponibili", + "machine_learning_availability_checks_enabled": "Attiva verifiche di disponibilità", + "machine_learning_availability_checks_interval": "Intervallo di verifica", + "machine_learning_availability_checks_interval_description": "Intervallo (ms) tra le verifiche di disponibilità", + "machine_learning_availability_checks_timeout": "Timeout richiesta", + "machine_learning_availability_checks_timeout_description": "Timeout (ms) per le verifiche di disponibilità", "machine_learning_clip_model": "Modello CLIP", "machine_learning_clip_model_description": "Il nome del modello CLIP mostrato qui. Nota che devi rieseguire il processo 'Ricerca Intelligente' per tutte le immagini al cambio del modello.", "machine_learning_duplicate_detection": "Rilevamento Duplicati", @@ -130,7 +138,7 @@ "machine_learning_duplicate_detection_enabled_description": "Se disattivo, risorse perfettamente identiche saranno comunque deduplicate.", "machine_learning_duplicate_detection_setting_description": "Utilizza i CLIP embeddings per trovare possibili duplicati", "machine_learning_enabled": "Attiva machine learning", - "machine_learning_enabled_description": "Se disabilitato, tutte le funzioni di ML saranno disabilitate ignorando le importazioni sottostanti.", + "machine_learning_enabled_description": "Se disabilitato, tutte le funzioni di ML saranno disabilitate ignorando le impostazioni sottostanti.", "machine_learning_facial_recognition": "Riconoscimento Facciale", "machine_learning_facial_recognition_description": "Rileva, riconosci e raggruppa volti nelle immagini", "machine_learning_facial_recognition_model": "Modello di riconoscimento facciale", @@ -143,7 +151,7 @@ "machine_learning_max_recognition_distance_description": "La distanza massima tra due volti per essere considerati la stessa persona, che varia da 0 a 2. Abbassare questo valore può prevenire l'etichettatura di due persone come se fossero la stessa persona, mentre aumentarlo può prevenire l'etichettatura della stessa persona come se fossero due persone diverse. Nota che è più facile unire due persone che separare una persona in due, quindi è preferibile mantenere una soglia più bassa quando possibile.", "machine_learning_min_detection_score": "Punteggio minimo di rilevazione", "machine_learning_min_detection_score_description": "Punteggio di confidenza minimo per rilevare un volto, da 0 a 1. Valori più bassi rileveranno più volti, ma potrebbero generare risultati fasulli.", - "machine_learning_min_recognized_faces": "Minimo volti rilevati", + "machine_learning_min_recognized_faces": "Minimo numero di volti rilevati", "machine_learning_min_recognized_faces_description": "Il numero minimo di volti riconosciuti per creare una persona. Aumentando questo valore si rende il riconoscimento facciale più preciso, ma aumenta la possibilità che un volto non venga assegnato a una persona.", "machine_learning_settings": "Impostazioni Machine Learning", "machine_learning_settings_description": "Gestisci le impostazioni e le funzionalità del machine learning", @@ -164,7 +172,7 @@ "map_reverse_geocoding": "Geocodifica inversa", "map_reverse_geocoding_enable_description": "Abilita geocodifica inversa", "map_reverse_geocoding_settings": "Impostazioni Geocodifica Inversa", - "map_settings": "Impostazioni Mappa e Posizione", + "map_settings": "Mappa", "map_settings_description": "Gestisci impostazioni mappa", "map_style_description": "URL per un tema della mappa style.json", "memory_cleanup_job": "Pulizia dei vecchi Ricordi", @@ -181,14 +189,14 @@ "nightly_tasks_cluster_new_faces_setting": "Raggruppa nuovi volti", "nightly_tasks_database_cleanup_setting": "Processi di pulizia del database", "nightly_tasks_database_cleanup_setting_description": "Ripulisci il database da file vecchi e scaduti", - "nightly_tasks_generate_memories_setting": "Genera ricordi", - "nightly_tasks_generate_memories_setting_description": "Genera nuovi ricordi a partire dalle risorse", + "nightly_tasks_generate_memories_setting": "Genera Ricordi", + "nightly_tasks_generate_memories_setting_description": "Genera nuovi Ricordi a partire dalle risorse", "nightly_tasks_missing_thumbnails_setting": "Genera anteprime mancanti", "nightly_tasks_missing_thumbnails_setting_description": "Metti in coda le risorse senza miniatura per la generazione delle anteprime", "nightly_tasks_settings": "Impostazioni delle attività notturne", "nightly_tasks_settings_description": "Gestisci attività notturne", - "nightly_tasks_start_time_setting": "Tempo di avvio", - "nightly_tasks_start_time_setting_description": "Il tempo in cui il server fa partire le attività notturne", + "nightly_tasks_start_time_setting": "Orario di avvio", + "nightly_tasks_start_time_setting_description": "L'orario in cui il server fa partire le attività notturne", "nightly_tasks_sync_quota_usage_setting": "Sincronizza la quota di utilizzo", "nightly_tasks_sync_quota_usage_setting_description": "Aggiorna la quota di spazio dell'utente in base all'utilizzo corrente", "no_paths_added": "Nessun percorso aggiunto", @@ -199,10 +207,10 @@ "notification_email_from_address_description": "Indirizzo email del mittente, ad esempio: \"Immich Photo Server \". Assicurati di utilizzare un indirizzo da cui sei autorizzato a inviare email.", "notification_email_host_description": "Host del server email (es. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignora errori di certificato", - "notification_email_ignore_certificate_errors_description": "Ignora errori di validazione del certificato TLS (sconsigliato)", + "notification_email_ignore_certificate_errors_description": "Ignora errori TLS di validazione del certificato (sconsigliato)", "notification_email_password_description": "Password da usare per l'autenticazione con il server email", "notification_email_port_description": "Porta del server email (es. 25, 465, 587)", - "notification_email_sent_test_email_button": "Invia email di test e salva", + "notification_email_sent_test_email_button": "Invia email di prova e salva", "notification_email_setting_description": "Impostazioni per le notifiche via email", "notification_email_test_email": "Invia email di prova", "notification_email_test_email_failed": "Impossibile inviare email di prova, controlla i valori inseriti", @@ -218,14 +226,14 @@ "oauth_button_text": "Testo pulsante", "oauth_client_secret_description": "Richiesto se PKCE (Proof Key for Code Exchange) non è supportato dal provider OAuth", "oauth_enable_description": "Login con OAuth", - "oauth_mobile_redirect_uri": "URI reindirizzamento mobile", - "oauth_mobile_redirect_uri_override": "Sovrascrivi URI reindirizzamento cellulare", + "oauth_mobile_redirect_uri": "URI di reindirizzamento per app mobile", + "oauth_mobile_redirect_uri_override": "Sovrascrivi URI di reindirizzamento per app mobile", "oauth_mobile_redirect_uri_override_description": "Abilita quando il gestore OAuth non consente un URL come ''{callback}''", "oauth_role_claim": "Claim del ruolo", "oauth_role_claim_description": "Concedi automaticamente l'accesso come amministratore in base alla presenza di questo claim. Il claim può essere 'user' o 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Gestisci impostazioni di login OAuth", - "oauth_settings_more_details": "Per più dettagli riguardo a questa funzionalità, consulta la documentazione.", + "oauth_settings_more_details": "Per maggiori informazioni su questa funzionalità, consulta la documentazione.", "oauth_storage_label_claim": "Dichiarazione di ambito(claim) etichetta archiviazione", "oauth_storage_label_claim_description": "Imposta automaticamente l'etichetta dell'archiviazione dell'utente al valore di questa dichiarazione di ambito(claim).", "oauth_storage_quota_claim": "Dichiarazione di ambito(claim) limite archiviazione", @@ -240,7 +248,7 @@ "paths_validated_successfully": "Percorsi validati con successo", "person_cleanup_job": "Pulizia Persona", "quota_size_gib": "Dimensione Archiviazione (GiB)", - "refreshing_all_libraries": "Aggiorna tutte le librerie", + "refreshing_all_libraries": "Aggiornando tutte le librerie", "registration": "Registrazione amministratore", "registration_description": "Poiché sei il primo utente del sistema, sarai assegnato come Amministratore e sarai responsabile dei task amministrativi, e utenti aggiuntivi saranno creati da te.", "require_password_change_on_login": "Richiedi all'utente di cambiare password al primo accesso", @@ -269,11 +277,11 @@ "storage_template_migration": "Migrazione modello archiviazione", "storage_template_migration_description": "Applica il {template} attuale agli asset caricati in precedenza", "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifiche retroattivamente esegui {job}.", - "storage_template_migration_job": "Processo Migrazione Modello di Archiviazione", - "storage_template_more_details": "Per maggiori informazioni riguardo a questa funzionalità, consulta il Modello Archiviazione e le sue conseguenze", + "storage_template_migration_job": "Processo di migrazione del Modello di Archiviazione", + "storage_template_more_details": "Per maggiori informazioni riguardo a questa funzionalità, consulta il Modello di Archiviazione e le sue conseguenze", "storage_template_onboarding_description_v2": "Se attiva, questa funzionalità organizzerà automaticamente i file utilizzando un modello definito dall'utente. Per maggiori informazioni, consultare la documentazione.", "storage_template_path_length": "Limite approssimativo lunghezza percorso: {length, number}/{limit, number}", - "storage_template_settings": "Modello Archiviazione", + "storage_template_settings": "Modello di Archiviazione", "storage_template_settings_description": "Gestisci la struttura delle cartelle e il nome degli asset caricati", "storage_template_user_label": "{label} è l'etichetta di archiviazione dell'utente", "system_settings": "Impostazioni di sistema", @@ -283,7 +291,7 @@ "template_email_invite_album": "Modello di invito all'album", "template_email_preview": "Anteprima", "template_email_settings": "Template Email", - "template_email_update_album": "Modello di aggiornamento dell'album", + "template_email_update_album": "Aggiorna template dell'album", "template_email_welcome": "Modello di email di benvenuto", "template_settings": "Templates Notifiche", "template_settings_description": "Gestisci i modelli personalizzati per le notifiche", @@ -387,15 +395,15 @@ "admin_password": "Password Amministratore", "administration": "Amministrazione", "advanced": "Avanzate", - "advanced_settings_beta_timeline_subtitle": "Prova la nuova esperienza dell'app", - "advanced_settings_beta_timeline_title": "Timeline beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa questa opzione per filtrare i contenuti multimediali durante la sincronizzazione in base a criteri alternativi. Prova questa opzione solo se riscontri problemi con il rilevamento di tutti gli album da parte dell'app.", "advanced_settings_enable_alternate_media_filter_title": "[SPERIMENTALE] Usa un filtro alternativo per la sincronizzazione degli album del dispositivo", "advanced_settings_log_level_title": "Livello log: {level}", - "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini locali. Attivare questa impostazione per caricare invece le immagini remote.", + "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono estremamente lenti a caricare le miniature da risorse locali. Attiva 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": "Header Proxy", + "advanced_settings_readonly_mode_subtitle": "Abilita la modalità di sola lettura in cui le foto possono essere solo visualizzate, mentre funzioni come la selezione di più immagini, la condivisione, la trasmissione e l'eliminazione sono tutte disabilitate. Abilita/Disabilita la sola lettura tramite l'avatar dell'utente dalla schermata principale", + "advanced_settings_readonly_mode_title": "Modalità di sola lettura", "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 quando l'azione è stata fatta via web", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Sicuro di voler rimuovere l'utente {user}?", "album_search_not_found": "Nessun album trovato corrispondente alla tua ricerca", "album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.", + "album_summary": "Sommario Album", "album_updated": "Album aggiornato", "album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media", "album_user_left": "{album} abbandonato", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Disconnetti", "app_settings": "Impostazioni Applicazione", "appears_in": "Compare in", + "apply_count": "Applica ({count, number})", "archive": "Archivio", "archive_action_prompt": "Aggiunti {count} elementi all'Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Elemento ripristinato con successo", "asset_skipped": "Saltato", "asset_skipped_in_trash": "Nel cestino", + "asset_trashed": "Asset cestinato", + "asset_troubleshoot": "Risoluzione dei problemi dell'asset", "asset_uploaded": "Caricato", "asset_uploading": "Caricamento…", "asset_viewer_settings_subtitle": "Gestisci le impostazioni del visualizzatore della galleria", @@ -502,6 +514,7 @@ "assets_added_to_album_count": "{count, plural, one {# asset aggiunto} other {# asset aggiunti}} all'album", "assets_added_to_albums_count": "Aggiunto {assetTotal, plural, one {# elemento} other {# elementi}} a {albumTotal, plural, one {# album} other {# album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {L'elemento} other {Gli elementi}} non possono essere aggiunti all'album", + "assets_cannot_be_added_to_albums": "Non é stato possibile aggiungere {count, plural, one {l'elemento} other {gli elementi}} a nessun album", "assets_count": "{count, plural, one {# elemento} other {# elementi}}", "assets_deleted_permanently": "{count} elementi cancellati definitivamente", "assets_deleted_permanently_from_server": "{count} elementi cancellati definitivamente dal server Immich", @@ -518,14 +531,17 @@ "assets_trashed_count": "{count, plural, one {Spostato # asset} other {Spostati # assets}} nel cestino", "assets_trashed_from_server": "{count} elementi cestinati dal server Immich", "assets_were_part_of_album_count": "{count, plural, one {L'asset era} other {Gli asset erano}} già parte dell'album", + "assets_were_part_of_albums_count": "{count, plural, one {L'elemento fa} other {Gli elementi fanno}} già parte degli album", "authorized_devices": "Dispositivi autorizzati", "automatic_endpoint_switching_subtitle": "Connetti localmente alla rete Wi-Fi specificata, se disponibile; altrimenti utilizza connessioni alternative", "automatic_endpoint_switching_title": "Cambio automatico di URL", "autoplay_slideshow": "Avvio automatico presentazione", "back": "Indietro", "back_close_deselect": "Indietro, chiudi o deseleziona", + "background_backup_running_error": "Il backup in background è attualmente in esecuzione, impossibile avviare il backup manuale", "background_location_permission": "Permesso di localizzazione in background", "background_location_permission_content": "Per fare in modo che sia possibile cambiare rete quando è in esecuzione in background, Immich deve *sempre* avere accesso alla tua posizione precisa in modo da poter leggere il nome della rete Wi-Fi", + "background_options": "Opzioni sfondo", "backup": "Backup", "backup_album_selection_page_albums_device": "Album sul dispositivo ({count})", "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere", @@ -533,6 +549,7 @@ "backup_album_selection_page_select_albums": "Seleziona gli album", "backup_album_selection_page_selection_info": "Informazioni sulla selezione", "backup_album_selection_page_total_assets": "Numero totale delle risorse", + "backup_albums_sync": "Sincronizzazione album di backup", "backup_all": "Tutti", "backup_background_service_backup_failed_message": "È stato impossibile fare il backup dei contenuti. Riprovo…", "backup_background_service_connection_failed_message": "Impossibile connettersi al server. Riprovo…", @@ -592,8 +609,6 @@ "backup_setting_subtitle": "Gestisci le impostazioni di upload in primo piano e in background", "backup_settings_subtitle": "Gestisci le impostazioni di caricamento", "backward": "Indietro", - "beta_sync": "Status sincronizzazione beta", - "beta_sync_subtitle": "Gestisci il nuovo sistema di sincronizzazione", "biometric_auth_enabled": "Autenticazione biometrica attivata", "biometric_locked_out": "Sei stato bloccato dall'autenticazione biometrica", "biometric_no_options": "Nessuna opzione biometrica disponibile", @@ -603,7 +618,7 @@ "blurred_background": "Sfondo sfocato", "bugs_and_feature_requests": "Bug & Richieste di nuove funzionalità", "build": "Compilazione", - "build_image": "Compila Immagine", + "build_image": "Immagine Compilata", "bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!", "bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.", "bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.", @@ -651,6 +666,8 @@ "change_pin_code": "Cambia il codice PIN", "change_your_password": "Modifica la tua password", "changed_visibility_successfully": "Visibilità modificata con successo", + "charging": "In carica", + "charging_requirement_mobile_backup": "Il backup in background richiede che il dispositivo sia in carica", "check_corrupt_asset_backup": "Verifica la presenza di backup di asset corrotti", "check_corrupt_asset_backup_button": "Effettua controllo", "check_corrupt_asset_backup_description": "Effettua questo controllo solo sotto rete Wi-Fi e quando tutti gli asset sono stati sottoposti a backup. La procedura potrebbe impiegare qualche minuto.", @@ -682,7 +699,7 @@ "comments_and_likes": "Commenti & mi piace", "comments_are_disabled": "I commenti sono disabilitati", "common_create_new_album": "Crea nuovo Album", - "common_server_error": "Si prega di controllare la connessione network, che il server sia raggiungibile e che le versione del server e app sono gli stessi.", + "common_server_error": "Verifica la connessione di rete, assicurati che il server sia raggiungibile e che le versioni dell’app e del server siano compatibili.", "completed": "Completato", "confirm": "Conferma", "confirm_admin_password": "Conferma password dell'amministratore", @@ -717,7 +734,7 @@ "copy_to_clipboard": "Copia negli appunti", "country": "Nazione", "cover": "Riempi la finestra", - "covers": "Copre", + "covers": "Copertine", "create": "Crea", "create_album": "Crea album", "create_album_page_untitled": "Senza titolo", @@ -733,10 +750,11 @@ "create_shared_album_page_share_select_photos": "Seleziona foto", "create_shared_link": "Crea link condiviso", "create_tag": "Crea tag", - "create_tag_description": "Crea un nuovo tag. Per i tag annidati, si prega di inserire il percorso completo del tag tra cui barre oblique.", + "create_tag_description": "Crea un nuovo tag. Per i tag nidificati, inserisci il percorso completo del tag includendo le barre oblique (/).", "create_user": "Crea utente", "created": "Creato", "created_at": "Creato il", + "creating_linked_albums": "Creazione di album collegati...", "crop": "Ritaglia", "curated_object_page_title": "Oggetti", "current_device": "Dispositivo attuale", @@ -749,9 +767,9 @@ "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Scuro", "dark_theme": "Imposta tema scuro", - "date_after": "Data dopo", + "date_after": "Dopo la data", "date_and_time": "Data e ora", - "date_before": "Data prima", + "date_before": "Prima della data", "date_format": "E, d LLL, y • hh:mm", "date_of_birth_saved": "Data di nascita salvata con successo", "date_range": "Intervallo di date", @@ -886,7 +904,9 @@ "error": "Errore", "error_change_sort_album": "Errore nel cambiare l'ordine di degli album", "error_delete_face": "Errore nel cancellare la faccia dalla foto", + "error_getting_places": "Errore durante il recupero dei luoghi", "error_loading_image": "Errore nel caricamento dell'immagine", + "error_loading_partners": "Errore durante il caricamento dei partner: {error}", "error_saving_image": "Errore: {error}", "error_tag_face_bounding_box": "Errore durante il tag del volto - impossibile ricavare le coordinate del riquadro", "error_title": "Errore - Qualcosa è andato storto", @@ -1051,6 +1071,7 @@ "favorites_page_no_favorites": "Nessun preferito", "feature_photo_updated": "Foto in evidenza aggiornata", "features": "Funzionalità", + "features_in_development": "Funzionalità in fase di sviluppo", "features_setting_description": "Gestisci le funzionalità dell'app", "file_name": "Nome file", "file_name_or_extension": "Nome file o estensione", @@ -1060,6 +1081,7 @@ "filter_people": "Filtra persone", "filter_places": "Filtra luoghi", "find_them_fast": "Trovale velocemente con la ricerca", + "first": "Primo", "fix_incorrect_match": "Correggi corrispondenza errata", "folder": "Cartella", "folder_not_found": "Cartella non trovata", @@ -1070,12 +1092,15 @@ "gcast_enabled": "Google Cast Abilitato", "gcast_enabled_description": "Questa funzione carica risorse esterne da Google per poter funzionare.", "general": "Generale", + "geolocation_instruction_location": "Fai clic su una risorsa con coordinate GPS per utilizzare la sua posizione oppure seleziona una posizione direttamente dalla mappa", "get_help": "Chiedi Aiuto", "get_wifiname_error": "Non sono riuscito a recuperare il nome della rete Wi-Fi. Accertati di aver concesso i permessi necessari e di essere connesso ad una rete Wi-Fi", "getting_started": "Iniziamo", "go_back": "Torna indietro", "go_to_folder": "Vai alla cartella", "go_to_search": "Vai alla ricerca", + "gps": "GPS", + "gps_missing": "No GPS", "grant_permission": "Concedi permesso", "group_albums_by": "Raggruppa album in base a...", "group_country": "Raggruppa per paese", @@ -1148,7 +1173,7 @@ "in_archive": "In archivio", "include_archived": "Includi Archiviati", "include_shared_albums": "Includi album condivisi", - "include_shared_partner_assets": "Includi asset condivisi del compagno", + "include_shared_partner_assets": "Includi elementi condivisi dai compagni", "individual_share": "Condivisione individuale", "individual_shares": "Condivisioni individuali", "info": "Info", @@ -1178,9 +1203,10 @@ "language": "Lingua", "language_no_results_subtitle": "Prova a cambiare i tuoi termini di ricerca", "language_no_results_title": "Linguaggi non trovati", - "language_search_hint": "Cerca linguaggi...", + "language_search_hint": "Cerca una lingua...", "language_setting_description": "Seleziona la tua lingua predefinita", "large_files": "File pesanti", + "last": "Ultimo", "last_seen": "Ultimo accesso", "latest_version": "Ultima Versione", "latitude": "Latitudine", @@ -1210,6 +1236,7 @@ "local": "Locale", "local_asset_cast_failed": "Impossibile trasmettere una risorsa che non è caricata sul server", "local_assets": "Risorsa locale", + "local_media_summary": "Riepilogo dei Media Locali", "local_network": "Rete locale", "local_network_sheet_info": "L'app si collegherà al server tramite questo URL quando è in uso la rete Wi-Fi specificata", "location_permission": "Permesso di localizzazione", @@ -1221,6 +1248,7 @@ "location_picker_longitude_hint": "Inserisci la longitudine qui", "lock": "Rendi privato", "locked_folder": "Cartella Privata", + "log_detail_title": "Dettaglio dei Log", "log_out": "Esci", "log_out_all_devices": "Disconnetti tutti i dispositivi", "logged_in_as": "Effettuato l'accesso come {user}", @@ -1251,6 +1279,7 @@ "login_password_changed_success": "Password aggiornata con successo", "logout_all_device_confirmation": "Sei sicuro di volerti disconnettere da tutti i dispositivi?", "logout_this_device_confirmation": "Sei sicuro di volerti disconnettere da questo dispositivo?", + "logs": "Logs", "longitude": "Longitudine", "look": "Guarda", "loop_videos": "Riproduci video in loop", @@ -1258,6 +1287,7 @@ "main_branch_warning": "Stai utilizzando una versione di sviluppo. Ti consigliamo vivamente di utilizzare una versione di rilascio!", "main_menu": "Menu Principale", "make": "Produttore", + "manage_geolocation": "Gestisci posizione", "manage_shared_links": "Gestisci link condivisi", "manage_sharing_with_partners": "Gestisci la condivisione con i compagni", "manage_the_app_settings": "Gestisci le impostazioni dell'applicazione", @@ -1292,6 +1322,7 @@ "mark_as_read": "Segna come letto", "marked_all_as_read": "Segnato tutto come letto", "matches": "Corrispondenze", + "matching_assets": "Assets Corrispondenti", "media_type": "Tipo Media", "memories": "Ricordi", "memories_all_caught_up": "Tutto a posto", @@ -1330,8 +1361,9 @@ "my_albums": "I miei album", "name": "Nome", "name_or_nickname": "Nome o soprannome", - "network_requirement_photos_upload": "Utilizzare i dati del cellulare per il backup delle foto", - "network_requirement_videos_upload": "Utilizzare i dati del cellulare per il backup dei video", + "network_requirement_photos_upload": "Utilizza la connessione dati per il backup delle foto", + "network_requirement_videos_upload": "Utilizza la connessione dati per il backup dei video", + "network_requirements": "Requisiti di rete", "network_requirements_updated": "Requisiti di rete modificati, coda di backup reimpostata", "networking_settings": "Rete", "networking_subtitle": "Gestisci le impostazioni riguardanti gli endpoint del server", @@ -1342,6 +1374,7 @@ "new_person": "Nuova persona", "new_pin_code": "Nuovo codice PIN", "new_pin_code_subtitle": "Questa è la prima volta che accedi alla cartella privata. Crea un codice PIN per accedere in modo sicuro a questa pagina", + "new_timeline": "Nuova Timeline", "new_user_created": "Nuovo utente creato", "new_version_available": "NUOVA VERSIONE DISPONIBILE", "newest_first": "Prima recenti", @@ -1355,20 +1388,25 @@ "no_assets_message": "CLICCA PER CARICARE LA TUA PRIMA FOTO", "no_assets_to_show": "Nessuna risorsa da mostrare", "no_cast_devices_found": "Nessun dispositivo di trasmissione trovato", + "no_checksum_local": "Nessun checksum disponibile: impossibile recuperare gli assets locali", + "no_checksum_remote": "Nessun checksum disponibile: impossibile recuperare l'asset remoto", "no_duplicates_found": "Nessun duplicato trovato.", "no_exif_info_available": "Nessuna informazione exif disponibile", "no_explore_results_message": "Carica più foto per esplorare la tua collezione.", "no_favorites_message": "Aggiungi preferiti per trovare facilmente le tue migliori foto e video", "no_libraries_message": "Crea una libreria esterna per vedere le tue foto e i tuoi video", + "no_local_assets_found": "Nessun asset locale trovato con questo checksum", "no_locked_photos_message": "Le foto e i video nella cartella privata sono nascosti e non vengono visualizzati mentre navighi o cerchi nella tua libreria.", "no_name": "Nessun nome", "no_notifications": "Nessuna notifica", "no_people_found": "Nessuna persona trovata", "no_places": "Nessun posto", + "no_remote_assets_found": "Nessun asset remoto trovato con questo checksum", "no_results": "Nessun risultato", "no_results_description": "Prova ad usare un sinonimo oppure una parola chiave più generica", "no_shared_albums_message": "Crea un album per condividere foto e video con le persone nella tua rete", "no_uploads_in_progress": "Nessun upload in corso", + "not_available": "N/A", "not_in_any_album": "In nessun album", "not_selected": "Non selezionato", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Per aggiungere l'etichetta dell'archiviazione agli asset caricati in precedenza, esegui", @@ -1403,6 +1441,8 @@ "open_the_search_filters": "Apri filtri di ricerca", "options": "Opzioni", "or": "o", + "organize_into_albums": "Organizza all'interno degli albums", + "organize_into_albums_description": "Inserisci le foto esistenti all'interno degli albums utilizzando le attuale impostazioni di sincronizzazione", "organize_your_library": "Organizza la tua libreria", "original": "originale", "other": "Altro", @@ -1488,6 +1528,7 @@ "port": "Porta", "preferences_settings_subtitle": "Gestisci le preferenze dell'app", "preferences_settings_title": "Preferenze", + "preparing": "Preparando", "preset": "Preimpostazione", "preview": "Anteprima", "previous": "Precedente", @@ -1504,6 +1545,7 @@ "profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Aggiorna all'ultima versione minore.", "profile_drawer_client_server_up_to_date": "Client e server sono aggiornati", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Modalità di sola lettura abilitata. Tieni premuto sull'avatar dell'utente per disabilitarla.", "profile_drawer_server_out_of_date_major": "Il server non è aggiornato. Aggiorna all'ultima versione principale.", "profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Aggiorna all'ultima versione minore.", "profile_image_of_user": "Immagine profilo di {user}", @@ -1542,6 +1584,7 @@ "purchase_server_description_2": "Stato di Contributore", "purchase_server_title": "Server", "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", + "query_asset_id": "Esegui una query sull'ID dell'asset", "queue_status": "Messi in coda {count}/{total}", "rating": "Valutazione a stelle", "rating_clear": "Crea valutazione", @@ -1549,6 +1592,9 @@ "rating_description": "Visualizza la valutazione EXIF nel pannello informazioni", "reaction_options": "Impostazioni Reazioni", "read_changelog": "Leggi Riepilogo Modifiche", + "readonly_mode_disabled": "Modalità di sola lettura disabilitata", + "readonly_mode_enabled": "Modalità di sola lettura abilitata", + "ready_for_upload": "Pronto per il caricamento", "reassign": "Riassegna", "reassigned_assets_to_existing_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} {name, select, null {ad una persona esistente} other {a {name}}}", "reassigned_assets_to_new_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} ad una nuova persona", @@ -1560,7 +1606,7 @@ "recently_added_page_title": "Aggiunti di recente", "recently_taken": "Scattate di recente", "recently_taken_page_title": "Scattate di Recente", - "refresh": "Aggiorna", + "refresh": "Ricarica", "refresh_encoded_videos": "Ricarica video codificati", "refresh_faces": "Aggiorna volti", "refresh_metadata": "Ricarica metadati", @@ -1573,6 +1619,7 @@ "regenerating_thumbnails": "Rigenerando le anteprime", "remote": "Remoto", "remote_assets": "Risorse remote", + "remote_media_summary": "Riepilogo dei Media Remoti", "remove": "Rimuovi", "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} dall'album?", "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?", @@ -1625,6 +1672,7 @@ "restore_user": "Ripristina utente", "restored_asset": "Asset ripristinato", "resume": "Riprendi", + "resume_paused_jobs": "Riprendi {count, plural, one {# processo in pausa} other {# i processi in pausa}}", "retry_upload": "Riprova caricamento", "review_duplicates": "Esamina duplicati", "review_large_files": "Revisiona file pesanti", @@ -1673,11 +1721,11 @@ "search_no_people": "Nessuna persona", "search_no_people_named": "Nessuna persona chiamate \"{name}\"", "search_no_result": "Nessun risultato trovato, prova con un termine o combinazione diversi", - "search_options": "Opzioni Ricerca", + "search_options": "Opzioni di ricerca", "search_page_categories": "Categoria", "search_page_motion_photos": "Foto in movimento", - "search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile", - "search_page_no_places": "Nessun informazione sul luogo disponibile", + "search_page_no_objects": "Nessuna informazione sugli oggetti disponibile", + "search_page_no_places": "Nessuna informazione sui luoghi disponibile", "search_page_screenshots": "Screenshot", "search_page_search_photos_videos": "Ricerca le tue foto e i tuoi video", "search_page_selfies": "Selfie", @@ -1718,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album", "selected": "Selezionato", "selected_count": "{count, plural, one {# selezionato} other {# selezionati}}", + "selected_gps_coordinates": "Coordinate GPS selezionate", "send_message": "Manda messaggio", "send_welcome_email": "Invia email di benvenuto", "server_endpoint": "Server endpoint", @@ -1846,6 +1895,7 @@ "show_slideshow_transition": "Mostra la transizione della presentazione", "show_supporter_badge": "Medaglia di Contributore", "show_supporter_badge_description": "Mostra la medaglia di contributore", + "show_text_search_menu": "Mostra il menu di ricerca del testo", "shuffle": "Casuale", "sidebar": "Barra laterale", "sidebar_display_description": "Visualizzare un link alla vista nella barra laterale", @@ -1871,11 +1921,12 @@ "stack_action_prompt": "{count} elementi raggruppati", "stack_duplicates": "Raggruppa i duplicati", "stack_select_one_photo": "Seleziona una foto principale per il gruppo", - "stack_selected_photos": "Impila foto selezionate", + "stack_selected_photos": "Raggruppa foto selezionate", "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # asset}}", "stacktrace": "Traccia dell'errore", "start": "Avvia", "start_date": "Data di inizio", + "start_date_before_end_date": "La data di inizio deve essere precedente alla data di fine", "state": "Provincia", "status": "Stato", "stop_casting": "Interrompi trasmissione", @@ -1900,6 +1951,8 @@ "sync_albums_manual_subtitle": "Sincronizza tutti i video e le foto caricati con gli album di backup selezionati", "sync_local": "Sincronizza gli elementi locali", "sync_remote": "Sincronizza gli elementi remoti", + "sync_status": "Stato di Sincronizzazione", + "sync_status_subtitle": "Visualizza e gestisci il sistema di sincronizzazione", "sync_upload_album_setting_subtitle": "Crea e carica le tue foto e video sull'album selezionato in Immich", "tag": "Tag", "tag_assets": "Tagga risorse", @@ -1937,7 +1990,9 @@ "to_change_password": "Modifica password", "to_favorite": "Preferito", "to_login": "Accedi", + "to_multi_select": "per selezione multipla", "to_parent": "Sali di un livello", + "to_select": "per selezionare", "to_trash": "Cancella", "toggle_settings": "Attiva/disattiva impostazioni", "total": "Totale", @@ -1957,6 +2012,7 @@ "trash_page_select_assets_btn": "Seleziona elemento", "trash_page_title": "Cestino ({count})", "trashed_items_will_be_permanently_deleted_after": "Gli elementi cestinati saranno eliminati definitivamente dopo {days, plural, one {# giorno} other {# giorni}}.", + "troubleshoot": "Risoluzione dei problemi", "type": "Tipo", "unable_to_change_pin_code": "Impossibile cambiare il codice PIN", "unable_to_setup_pin_code": "Impossibile configurare il codice PIN", @@ -1982,11 +2038,12 @@ "unselect_all": "Deseleziona tutto", "unselect_all_duplicates": "Deseleziona tutti i duplicati", "unselect_all_in": "Deseleziona tutto in {group}", - "unstack": "Rimuovi dal gruppo", + "unstack": "Separa dal gruppo", "unstack_action_prompt": "{count} separati", "unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # asset}}", "untagged": "Senza tag", "up_next": "Prossimo", + "update_location_action_prompt": "Aggiorna la posizione di {count} risorse selezionate con:", "updated_at": "Aggiornato il", "updated_password": "Password aggiornata", "upload": "Carica", @@ -2053,11 +2110,12 @@ "view_next_asset": "Visualizza risorsa successiva", "view_previous_asset": "Visualizza risorsa precedente", "view_qr_code": "Visualizza Codice QR", + "view_similar_photos": "Visualizza le foto simili", "view_stack": "Visualizza Raggruppamento", "view_user": "Visualizza Utente", - "viewer_remove_from_stack": "Rimuovi dalla pila", + "viewer_remove_from_stack": "Rimuovi dal gruppo", "viewer_stack_use_as_main_asset": "Usa come risorsa principale", - "viewer_unstack": "Rimuovi dal gruppo", + "viewer_unstack": "Separa dal gruppo", "visibility_changed": "Visibilità modificata per {count, plural, one {# persona} other {# persone}}", "waiting": "In Attesa", "warning": "Attenzione", @@ -2068,8 +2126,9 @@ "wrong_pin_code": "Codice PIN errato", "year": "Anno", "years_ago": "{years, plural, one {# anno} other {# anni}} fa", - "yes": "Si", - "you_dont_have_any_shared_links": "Non è presente alcun link condiviso", + "yes": "Sì", + "you_dont_have_any_shared_links": "Non hai nessun link condiviso", "your_wifi_name": "Nome della tua rete Wi-Fi", - "zoom_image": "Ingrandisci immagine" + "zoom_image": "Ingrandisci immagine", + "zoom_to_bounds": "Ingrandisci fino ai bordi" } diff --git a/i18n/ja.json b/i18n/ja.json index f48da982cd..b03614d65c 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -28,6 +28,9 @@ "add_to_album": "アルバムに追加", "add_to_album_bottom_sheet_added": "{album}に追加", "add_to_album_bottom_sheet_already_exists": "{album}に追加済み", + "add_to_album_toggle": "{album}の選択を切り替え", + "add_to_albums": "アルバムに追加", + "add_to_albums_count": "{count}つのアルバムへ追加", "add_to_shared_album": "共有アルバムに追加", "add_url": "URLを追加", "added_to_archive": "アーカイブにしました", @@ -120,6 +123,13 @@ "logging_enable_description": "ログの有効化", "logging_level_description": "有効な場合に使用されるログ レベル。", "logging_settings": "ログ", + "machine_learning_availability_checks": "可用性の確認", + "machine_learning_availability_checks_description": "利用可能な機械学習のサーバーを自動で検知し優先的に使用します", + "machine_learning_availability_checks_enabled": "可用性チェックを有効にする", + "machine_learning_availability_checks_interval": "チェックの間隔", + "machine_learning_availability_checks_interval_description": "可用性チェックの間隔(ミリ秒単位)", + "machine_learning_availability_checks_timeout": "リクエストタイムアウト", + "machine_learning_availability_checks_timeout_description": "可用性チェックのタイムアウト時間(ミリ秒単位)", "machine_learning_clip_model": "Clipモデル", "machine_learning_clip_model_description": "CLIP モデルの名前はここにリストされています。モデルを変更した場合は、すべてのイメージに対して「スマート検索」ジョブを再実行する必要があります。", "machine_learning_duplicate_detection": "重複検出", @@ -218,6 +228,7 @@ "oauth_mobile_redirect_uri": "モバイル用リダイレクトURI", "oauth_mobile_redirect_uri_override": "モバイル用リダイレクトURI(上書き)", "oauth_mobile_redirect_uri_override_description": "''{callback}''など、モバイルURIがOAuthプロバイダーによって許可されていない場合に有効にしてください", + "oauth_role_claim": "役職を主張する", "oauth_role_claim_description": "管理者になると申し立てが行われた場合、自動的に管理者アクセスがその人に付与されるようにします。", "oauth_settings": "OAuth", "oauth_settings_description": "OAuthログイン設定を管理します", @@ -383,15 +394,15 @@ "admin_password": "管理者パスワード", "administration": "管理", "advanced": "詳細設定", - "advanced_settings_beta_timeline_subtitle": "新しいアプリを体験してみましょう", - "advanced_settings_beta_timeline_title": "試験運用のタイムライン", "advanced_settings_enable_alternate_media_filter_subtitle": "別の基準に従ってメディアファイルにフィルターをかけて、同期を行います。アプリがすべてのアルバムを読み込んでくれない場合にのみ、この機能を試してください。", "advanced_settings_enable_alternate_media_filter_title": "[試験運用] 別のデバイスのアルバム同期フィルターを使用する", "advanced_settings_log_level_title": "ログレベル: {level}", - "advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションをに有効にする事により、サーバーから直接画像をロードすることが可能です。", + "advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションを有効にする事により、サーバーから直接画像をロードすることが可能です。", "advanced_settings_prefer_remote_title": "リモートを優先する", "advanced_settings_proxy_headers_subtitle": "プロキシヘッダを設定する", "advanced_settings_proxy_headers_title": "プロキシヘッダ", + "advanced_settings_readonly_mode_subtitle": "読み取り専用モードを有効にすると、写真の複数選択や、共有、削除、キャスト機能が無効になります。メインスクリーンのユーザーアバターから有効/無効を切り替えられます", + "advanced_settings_readonly_mode_title": "読み取り専用モード", "advanced_settings_self_signed_ssl_subtitle": "SSLのチェックをスキップする。自己署名証明書が必要です。", "advanced_settings_self_signed_ssl_title": "自己署名証明書を許可する", "advanced_settings_sync_remote_deletions_subtitle": "Webでこの操作を行った際に、自動的にこのデバイス上から当該アセットを削除または復元する", @@ -419,6 +430,7 @@ "album_remove_user_confirmation": "本当に{user}を削除しますか?", "album_search_not_found": "検索に一致するアルバムがありません", "album_share_no_users": "このアルバムを全てのユーザーと共有したか、共有するユーザーがいないようです。", + "album_summary": "アルバムのまとめ", "album_updated": "アルバム更新", "album_updated_setting_description": "共有アルバムに新しいアセットが追加されたとき通知を受け取る", "album_user_left": "{album} を去りました", @@ -457,6 +469,7 @@ "app_bar_signout_dialog_title": "サインアウト", "app_settings": "アプリ設定", "appears_in": "これらに含まれます", + "apply_count": "適用 ({count, number})", "archive": "アーカイブ", "archive_action_prompt": "アーカイブに{count}項目追加しました", "archive_or_unarchive_photo": "写真をアーカイブまたはアーカイブ解除", @@ -489,6 +502,8 @@ "asset_restored_successfully": "復元できました", "asset_skipped": "スキップ済", "asset_skipped_in_trash": "ゴミ箱の中", + "asset_trashed": "項目が削除されました", + "asset_troubleshoot": "項目をトラブルシューㇳ", "asset_uploaded": "アップロード済", "asset_uploading": "アップロード中…", "asset_viewer_settings_subtitle": "ギャラリービューアーに関する設定", @@ -496,7 +511,9 @@ "assets": "アセット", "assets_added_count": "{count, plural, one {#個} other {#個}}のアセットを追加しました", "assets_added_to_album_count": "{count, plural, one {#個} other {#個}}のアセットをアルバムに追加しました", + "assets_added_to_albums_count": "{assetTotal, plural, one {# 項目} other {# 項目}}を{albumTotal, plural, one {# つのアルバム} other {# つのアルバム}}に追加しました", "assets_cannot_be_added_to_album_count": "{count, plural, one {アセット} other {アセット}} はアルバムに追加できません", + "assets_cannot_be_added_to_albums": "{count, plural, one {項目} other {項目}} をどのアルバムにも追加できませんでした", "assets_count": "{count, plural, one {#個} other {#個}}のアセット", "assets_deleted_permanently": "{count}項目を完全に削除しました", "assets_deleted_permanently_from_server": "サーバー上の{count}項目を完全に削除しました", @@ -513,14 +530,17 @@ "assets_trashed_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました", "assets_trashed_from_server": "サーバー上の{count}項目をゴミ箱に移動しました", "assets_were_part_of_album_count": "{count, plural, one {個} other {個}}のアセットは既にアルバムの一部です", + "assets_were_part_of_albums_count": "{count, plural, one {項目は} other {項目は}}すでにアルバムに追加されているものでした", "authorized_devices": "認可済みデバイス", "automatic_endpoint_switching_subtitle": "指定されたWi-Fiに接続時のみローカル接続を行い、他のネットワーク下では通常通りの接続を行います", "automatic_endpoint_switching_title": "自動URL切り替え", "autoplay_slideshow": "スライドショーを自動再生", "back": "戻る", "back_close_deselect": "戻る、閉じる、選択解除", + "background_backup_running_error": "バックグラウンドのバックアップがすでに行われている最中です。そのため、マニュアルでのバックアップを開始することはできません。", "background_location_permission": "バックグラウンド位置情報アクセス", "background_location_permission_content": "正常にWi-Fiの名前(SSID)を獲得するにはアプリが常に詳細な位置情報にアクセスできる必要があります", + "background_options": "バックグラウンドの動作オプション", "backup": "バックアップ", "backup_album_selection_page_albums_device": "デバイス上のアルバム({count})", "backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外", @@ -528,6 +548,7 @@ "backup_album_selection_page_select_albums": "アルバムを選択", "backup_album_selection_page_selection_info": "選択・除外中のアルバム", "backup_album_selection_page_total_assets": "選択されたアルバムの写真と動画の数", + "backup_albums_sync": "アルバム同期状態をバックアップ", "backup_all": "すべて", "backup_background_service_backup_failed_message": "アップロードに失敗しました。リトライ中…", "backup_background_service_connection_failed_message": "サーバーに接続できません。リトライ中…", @@ -587,8 +608,6 @@ "backup_setting_subtitle": "アップロードに関する設定", "backup_settings_subtitle": "アップロード設定を管理", "backward": "新しい方へ", - "beta_sync": "同期の状態", - "beta_sync_subtitle": "同期の仕組みを管理", "biometric_auth_enabled": "生体認証を有効化しました", "biometric_locked_out": "生体認証により、アクセスできません", "biometric_no_options": "生体認証を利用できません", @@ -646,6 +665,8 @@ "change_pin_code": "PINコードを変更", "change_your_password": "パスワードを変更します", "changed_visibility_successfully": "非表示設定を正常に変更しました", + "charging": "充電中", + "charging_requirement_mobile_backup": "バックグラウンドでのバックアップを行うためには、デバイスが充電中である必要があります", "check_corrupt_asset_backup": "破損されている項目を探す", "check_corrupt_asset_backup_button": "チェックを行う", "check_corrupt_asset_backup_description": "写真や動画などが全てアップロードし終えてからWi-Fiに接続時のみチェックを行なってください。作業が完了するには数分かかる場合があります", @@ -699,7 +720,7 @@ "control_bottom_app_bar_edit_location": "位置情報を編集", "control_bottom_app_bar_edit_time": "撮影日時を編集", "control_bottom_app_bar_share_link": "共有リンク", - "control_bottom_app_bar_share_to": "次のユーザーに共有: ", + "control_bottom_app_bar_share_to": "次のユーザーに共有:", "control_bottom_app_bar_trash_from_immich": "ゴミ箱に入れる", "copied_image_to_clipboard": "画像をクリップボードにコピーしました。", "copied_to_clipboard": "クリップボードにコピーしました!", @@ -732,6 +753,7 @@ "create_user": "ユーザーを作成", "created": "作成", "created_at": "作成:", + "creating_linked_albums": "リンクされたアルバムを作成中・・・", "crop": "クロップ", "curated_object_page_title": "被写体", "current_device": "現在のデバイス", @@ -881,7 +903,9 @@ "error": "エラー", "error_change_sort_album": "アルバムの表示順の変更に失敗しました", "error_delete_face": "アセットから顔の削除ができませんでした", + "error_getting_places": "場所の取得に失敗しました", "error_loading_image": "画像の読み込みエラー", + "error_loading_partners": "パートナーの読み込みに失敗しました: {error}", "error_saving_image": "エラー: {error}", "error_tag_face_bounding_box": "顔の登録に失敗しました - 顔を囲む四角形の座標取得に失敗", "error_title": "エラー - 問題が発生しました", @@ -1046,6 +1070,7 @@ "favorites_page_no_favorites": "お気に入り登録された項目がありません", "feature_photo_updated": "人物画像が更新されました", "features": "機能", + "features_in_development": "開発中の機能", "features_setting_description": "アプリの機能を管理する", "file_name": "ファイル名", "file_name_or_extension": "ファイル名または拡張子", @@ -1055,6 +1080,7 @@ "filter_people": "人物を絞り込み", "filter_places": "場所をフィルター", "find_them_fast": "名前で検索して素早く発見", + "first": "はじめ", "fix_incorrect_match": "間違った一致を修正", "folder": "フォルダー", "folder_not_found": "フォルダーが見つかりませんでした", @@ -1065,12 +1091,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "この機能は動作のためにGoogleのリソースを読み込みます。", "general": "一般", + "geolocation_instruction_location": "位置情報付きの項目をクリックして、その位置情報を利用します。あるいは、地図上の地点を直接選ぶことも可能です", "get_help": "助けを求める", "get_wifiname_error": "Wi-Fiの名前(SSID)が入手できませんでした。Wi-Fiに繋がってるのと必要な権限を許可したか確認してください", "getting_started": "はじめる", "go_back": "戻る", "go_to_folder": "フォルダへ", "go_to_search": "検索へ", + "gps": "GPS", + "gps_missing": "GPS無", "grant_permission": "許可する", "group_albums_by": "これでアルバムをグループ化…", "group_country": "国でグループ化", @@ -1176,6 +1205,7 @@ "language_search_hint": "言語を検索", "language_setting_description": "優先言語を選択してください", "large_files": "大きいサイズのファイル", + "last": "最後", "last_seen": "最新の活動", "latest_version": "最新バージョン", "latitude": "緯度", @@ -1205,6 +1235,7 @@ "local": "ローカル", "local_asset_cast_failed": "サーバーにアップロードされていない項目はキャストできません", "local_assets": "ローカルの項目", + "local_media_summary": "ローカルメディアのまとめ", "local_network": "ローカルネットワーク", "local_network_sheet_info": "アプリは指定されたWi-Fiに繋がっている時サーバーへの接続を下記のURLで行います", "location_permission": "位置情報権限", @@ -1216,6 +1247,7 @@ "location_picker_longitude_hint": "経度を入力", "lock": "ロック", "locked_folder": "鍵付きフォルダー", + "log_detail_title": "ログの詳細", "log_out": "ログアウト", "log_out_all_devices": "全てのデバイスからログアウト", "logged_in_as": "{user}としてログイン中", @@ -1246,6 +1278,7 @@ "login_password_changed_success": "パスワードの変更に成功", "logout_all_device_confirmation": "本当に全てのデバイスからログアウトしますか?", "logout_this_device_confirmation": "本当にこのデバイスからログアウトしますか?", + "logs": "ログ", "longitude": "経度", "look": "見た目", "loop_videos": "動画をループ", @@ -1253,6 +1286,7 @@ "main_branch_warning": "開発版を使っているようです。リリース版の使用を強く推奨します!", "main_menu": "メインメニュー", "make": "メーカー", + "manage_geolocation": "位置情報を編集", "manage_shared_links": "共有済みのリンクを管理", "manage_sharing_with_partners": "パートナーとの共有を管理します", "manage_the_app_settings": "アプリの設定を管理します", @@ -1287,6 +1321,7 @@ "mark_as_read": "既読にする", "marked_all_as_read": "すべて既読にしました", "matches": "マッチ", + "matching_assets": "一致する項目", "media_type": "メディアタイプ", "memories": "メモリー", "memories_all_caught_up": "これで全部です", @@ -1327,6 +1362,7 @@ "name_or_nickname": "名前またはニックネーム", "network_requirement_photos_upload": "モバイル通信を使用して写真のバックアップを行う", "network_requirement_videos_upload": "モバイル通信を使用して動画のバックアップを行う", + "network_requirements": "ネットワークの要件", "network_requirements_updated": "ネットワークの条件が変更されたため、バックアップの順番待ちをリセットします", "networking_settings": "ネットワーク", "networking_subtitle": "サーバーエンドポイントに関する設定", @@ -1337,6 +1373,7 @@ "new_person": "新しい人物", "new_pin_code": "新しいPINコード", "new_pin_code_subtitle": "鍵付きフォルダーを利用するのが初めてのようです。PINコードを作成してください", + "new_timeline": "新たなタイムライン", "new_user_created": "新しいユーザーが作成されました", "new_version_available": "新しいバージョンが利用可能", "newest_first": "最新順", @@ -1350,20 +1387,25 @@ "no_assets_message": "クリックして最初の写真をアップロード", "no_assets_to_show": "表示する項目がありません", "no_cast_devices_found": "キャスト先のデバイスが見つかりません", + "no_checksum_local": "チェックサムが見つかりません - デバイス上の項目を取得できないようです", + "no_checksum_remote": "チェックサムが見つかりません - サーバー上の項目を取得できないようです", "no_duplicates_found": "重複は見つかりませんでした。", "no_exif_info_available": "exif情報が利用できません", "no_explore_results_message": "コレクションを探索するにはさらに写真をアップロードしてください。", "no_favorites_message": "お気に入り登録すると好きな写真や動画をすぐに見つけられます", "no_libraries_message": "あなたの写真や動画を表示するための外部ライブラリを作成しましょう", + "no_local_assets_found": "このチェックサムの項目はデバイス上に存在しません", "no_locked_photos_message": "鍵付きフォルダー内の写真や動画は通常のライブラリに表示されなくなります。", "no_name": "名前なし", "no_notifications": "通知なし", "no_people_found": "一致する人物が見つかりません", "no_places": "場所なし", + "no_remote_assets_found": "このチェックサムの項目はサーバー上に存在しません", "no_results": "結果がありません", "no_results_description": "同義語やより一般的なキーワードを試してください", "no_shared_albums_message": "アルバムを作成して写真や動画を共有しましょう", "no_uploads_in_progress": "アップロードは行われていません", + "not_available": "適用なし", "not_in_any_album": "どのアルバムにも入っていない", "not_selected": "選択なし", "note_apply_storage_label_to_previously_uploaded assets": "注意: 以前にアップロードしたアセットにストレージラベルを適用するには以下を実行してください", @@ -1398,6 +1440,8 @@ "open_the_search_filters": "検索フィルタを開く", "options": "オプション", "or": "または", + "organize_into_albums": "アルバムに追加して整理する", + "organize_into_albums_description": "既存の写真を、現在の同期設定に基づきアルバムに追加する", "organize_your_library": "ライブラリを整理", "original": "オリジナル", "other": "その他", @@ -1416,7 +1460,7 @@ "partner_page_no_more_users": "追加できるユーザーがもういません", "partner_page_partner_add_failed": "パートナーの追加に失敗", "partner_page_select_partner": "パートナーを選択", - "partner_page_shared_to_title": "次のユーザーと共有します: ", + "partner_page_shared_to_title": "次のユーザーと共有します:", "partner_page_stop_sharing_content": "{partner}は今後あなたの写真へアクセスできなくなります", "partner_sharing": "パートナとの共有", "partners": "パートナー", @@ -1457,9 +1501,9 @@ "permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichが写真のバックアップと管理を行うには、システム設定から写真と動画のアクセス権限を変更してください。", "permission_onboarding_request": "Immichは写真へのアクセス許可が必要です", "person": "人物", - "person_age_months": "{months, plural, one {# ヶ月} other {# ヶ月}} 前", - "person_age_year_months": "1 年, {months, plural, one {# ヶ月} other {# ヶ月}} 前", - "person_age_years": "{years, plural, other {# 年}}前", + "person_age_months": "生後 {months, plural, one {# ヶ月} other {# ヶ月}}", + "person_age_year_months": "1 歳と, {months, plural, one {# ヶ月} other {# ヶ月}}", + "person_age_years": "{years, plural, other {# 歳}}", "person_birthdate": "{date}生まれ", "person_hidden": "{name}{hidden, select, true { (非表示)} other {}}", "photo_shared_all_users": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。", @@ -1483,6 +1527,7 @@ "port": "ポートレート", "preferences_settings_subtitle": "アプリに関する設定", "preferences_settings_title": "設定", + "preparing": "準備中", "preset": "プリセット", "preview": "プレビュー", "previous": "前", @@ -1499,6 +1544,7 @@ "profile_drawer_client_out_of_date_minor": "アプリが更新されてません。最新のバージョンに更新してください", "profile_drawer_client_server_up_to_date": "すべて最新版です", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "読み取り専用モードが有効です。ユーザーのアイコンを長押しして読み取り専用モードを解除してください。", "profile_drawer_server_out_of_date_major": "サーバーが更新されてません。最新のバージョンに更新してください", "profile_drawer_server_out_of_date_minor": "サーバーが更新されてません。最新のバージョンに更新してください", "profile_image_of_user": "{user} のプロフィール画像", @@ -1537,6 +1583,7 @@ "purchase_server_description_2": "サポーターの状態", "purchase_server_title": "サーバー", "purchase_settings_server_activated": "サーバーのプロダクトキーは管理者に管理されています", + "query_asset_id": "順番待ちの項目ID", "queue_status": "順番待ち中 {count}/{total}", "rating": "星での評価", "rating_clear": "評価を取り消す", @@ -1544,6 +1591,9 @@ "rating_description": "情報欄にEXIFの評価を表示", "reaction_options": "リアクションの選択", "read_changelog": "変更履歴を読む", + "readonly_mode_disabled": "読み取り専用モード無効", + "readonly_mode_enabled": "読み取り専用モード有効", + "ready_for_upload": "アップロード準備完了", "reassign": "再割り当て", "reassigned_assets_to_existing_person": "{count, plural, one {#個} other {#個}}のアセットを{name, select, null {既存の人物} other {{name}}}に再割り当てしました", "reassigned_assets_to_new_person": "{count, plural, one {#個} other {#個}}のアセットを新しい人物に割り当てました", @@ -1568,6 +1618,7 @@ "regenerating_thumbnails": "サムネイルを再生成中", "remote": "リモート", "remote_assets": "リモートの項目", + "remote_media_summary": "サーバー上のメディアまとめ", "remove": "削除", "remove_assets_album_confirmation": "本当に{count, plural, one {#個} other {#個}}のアセットをアルバムから削除しますか?", "remove_assets_shared_link_confirmation": "本当にこの共有リンクから{count, plural, one {#個} other {#個}}のアセットを削除しますか?", @@ -1620,6 +1671,7 @@ "restore_user": "ユーザーを復元", "restored_asset": "項目を復元しました", "resume": "再開", + "resume_paused_jobs": "再開: {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "アップロードを再試行", "review_duplicates": "重複を調査", "review_large_files": "サイズの大きなファイルを見る", @@ -1713,11 +1765,12 @@ "select_user_for_sharing_page_err_album": "アルバム作成に失敗", "selected": "選択済み", "selected_count": "{count, plural, other {#個選択済み}}", + "selected_gps_coordinates": "選択された位置情報", "send_message": "メッセージを送信", "send_welcome_email": "ウェルカムメールを送信", "server_endpoint": "サーバーエンドポイント", "server_info_box_app_version": "アプリのバージョン", - "server_info_box_server_url": " サーバーのURL", + "server_info_box_server_url": "サーバーのURL", "server_offline": "サーバーがオフラインです", "server_online": "サーバーがオンラインです", "server_privacy": "サーバープライバシー", @@ -1841,6 +1894,7 @@ "show_slideshow_transition": "スライドショーのトランジションを表示", "show_supporter_badge": "サポーターバッジ", "show_supporter_badge_description": "サポーターバッジを表示", + "show_text_search_menu": "テキスト検索メニューを表示", "shuffle": "ランダム", "sidebar": "サイドバー", "sidebar_display_description": "サイドバーにビューへのリンクを表示", @@ -1871,6 +1925,7 @@ "stacktrace": "スタックトレース", "start": "開始", "start_date": "開始日", + "start_date_before_end_date": "開始日は終了日より前でなければなりません", "state": "都道府県", "status": "ステータス", "stop_casting": "キャストを停止", @@ -1895,6 +1950,8 @@ "sync_albums_manual_subtitle": "アップロード済みの全ての写真や動画を選択されたバックアップアルバムに同期する", "sync_local": "ローカルを同期", "sync_remote": "リモートを同期", + "sync_status": "同期の状態", + "sync_status_subtitle": "同期システムを確認・管理", "sync_upload_album_setting_subtitle": "サーバー上のアルバムの内容を端末上のアルバムと同期します (サーバーにアルバムが無い場合自動で作成されます。また、アップロードされていない写真や動画は同期されません)", "tag": "タグ付けする", "tag_assets": "アセットにタグ付けする", @@ -1932,7 +1989,9 @@ "to_change_password": "パスワードを変更", "to_favorite": "お気に入り", "to_login": "ログイン", + "to_multi_select": "複数選択", "to_parent": "上位の階層へ", + "to_select": "選択", "to_trash": "ゴミ箱", "toggle_settings": "設定をトグル", "total": "合計", @@ -1952,6 +2011,7 @@ "trash_page_select_assets_btn": "項目を選択", "trash_page_title": "ゴミ箱 ({count})", "trashed_items_will_be_permanently_deleted_after": "ゴミ箱に入れられたアイテムは{days, plural, one {#日} other {#日}}後に完全に削除されます。", + "troubleshoot": "トラブルシューティング", "type": "タイプ", "unable_to_change_pin_code": "PINコードを変更できませんでした", "unable_to_setup_pin_code": "PINコードをセットアップできませんでした", @@ -1982,6 +2042,7 @@ "unstacked_assets_count": "{count, plural, one {#個のアセット} other {#個のアセット}}をスタックから解除しました", "untagged": "タグを解除", "up_next": "次へ", + "update_location_action_prompt": "{count}項目を右記の位置情報にアップデートします:", "updated_at": "更新", "updated_password": "パスワードを更新しました", "upload": "アップロード", @@ -2000,7 +2061,7 @@ "upload_success": "アップロード成功、新しくアップロードされたアセットを見るにはページを更新してください。", "upload_to_immich": "Immichにアップロード ({count})", "uploading": "アップロード中", - "uploading_media": "メディアをアップロード", + "uploading_media": "メディアをアップロード中", "url": "URL", "usage": "使用容量", "use_biometric": "生体認証をご利用ください", @@ -2048,6 +2109,7 @@ "view_next_asset": "次のアセットを見る", "view_previous_asset": "前のアセットを見る", "view_qr_code": "QRコードを見る", + "view_similar_photos": "類似する写真を見る", "view_stack": "ビュースタック", "view_user": "ユーザーを見る", "viewer_remove_from_stack": "スタックから外す", @@ -2066,5 +2128,6 @@ "yes": "はい", "you_dont_have_any_shared_links": "共有リンクはありません", "your_wifi_name": "Wi-Fiの名前(SSID)", - "zoom_image": "画像を拡大" + "zoom_image": "画像を拡大", + "zoom_to_bounds": "画面端までズーム" } diff --git a/i18n/kk.json b/i18n/kk.json index 5dabe59a13..7025ae0d22 100644 --- a/i18n/kk.json +++ b/i18n/kk.json @@ -1,11 +1,21 @@ { "about": "Туралы", "account": "Тіркелгі", + "add": "қосу", + "add_a_description": "сипаттаманы қосу", + "add_a_location": "суретті түсірген жерді қосы", + "add_a_name": "Атын қосу", + "add_birthday": "Туған күнін қосу", + "add_location": "жерді қосу", + "add_more_users": "қосымша адамдарды тіркеу", + "add_partner": "жолдасты қосу", "add_photos": "суреттерді қосу", + "add_tag": "тегті қосу", "add_to": "қосу…", "add_to_album": "альбомға қосу", "add_to_album_bottom_sheet_added": "{album}'ға қосылған", "add_to_album_bottom_sheet_already_exists": "Онсыз да {album} болған", + "add_to_albums": "альбомдарға қосу", "add_to_shared_album": "бөліскен альбомға қосу", "add_url": "URL таңдау", "added_to_archive": "Архивке жіберілген", diff --git a/i18n/ko.json b/i18n/ko.json index 44acba3590..3707501ec8 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -28,6 +28,7 @@ "add_to_album": "앨범에 추가", "add_to_album_bottom_sheet_added": "{album}에 추가됨", "add_to_album_bottom_sheet_already_exists": "이미 {album}에 있음", + "add_to_album_bottom_sheet_some_local_assets": "몇 개의 로컬 항목이 앨범에 추가되지 않았습니다", "add_to_album_toggle": "{album} 선택/해제", "add_to_albums": "여러 앨범에 추가", "add_to_albums_count": "여러 앨범에 추가 ({count})", @@ -48,12 +49,12 @@ "backup_database": "데이터베이스 덤프 생성", "backup_database_enable_description": "데이터베이스 덤프 활성화", "backup_keep_last_amount": "보관할 이전 덤프 수", - "backup_onboarding_1_description": "클라우드나 다른 물리적 위치에 보관하는 외부 백업본", - "backup_onboarding_2_description": "서로 다른 장치에 저장된 로컬 사본. 여기에는 원본 파일과 해당 파일의 로컬 백업이 포함됩니다.", - "backup_onboarding_3_description": "원본 파일을 포함해 데이터의 총 사본은 3개입니다. 이 중 하나는 외부 백업이고 2개는 로컬 백업입니다.", - "backup_onboarding_description": "데이터를 보호하기 위해 3-2-1 백업 전략을 권장합니다. 완전한 백업을 위해 업로드한 사진 및 동영상뿐 아니라 Immich 데이터베이스도 함께 보관해야 합니다.", - "backup_onboarding_footer": "Immich 백업에 대한 자세한 내용은 문서를 참조하세요.", - "backup_onboarding_parts_title": "3-2-1 백업에는 다음이 포함됩니다:", + "backup_onboarding_1_description": "개는 클라우드나 다른 물리적 위치에 보관합니다.", + "backup_onboarding_2_description": "개는 서로 다른 로컬 장치에 보관하고,", + "backup_onboarding_3_description": "개의 데이터 사본을 만듭니다.", + "backup_onboarding_description": "소중한 데이터를 안전하게 보호하기 위해 3-2-1 백업 전략 사용을 권장합니다. Immich를 백업할 때 업로드한 사진 및 동영상뿐 아니라 데이터베이스도 함께 보관해야 한다는 점을 잊지 마세요.", + "backup_onboarding_footer": "Immich 백업에 대한 자세한 내용은 공식 문서를 참조하세요.", + "backup_onboarding_parts_title": "3-2-1 백업이란:", "backup_onboarding_title": "백업", "backup_settings": "데이터베이스 덤프 설정", "backup_settings_description": "데이터베이스 덤프 주기와 보관 기간을 설정합니다.", @@ -115,7 +116,7 @@ "library_scanning_description": "주기적인 라이브러리 스캔을 구성합니다.", "library_scanning_enable_description": "주기적인 라이브러리 스캔 활성화", "library_settings": "외부 라이브러리", - "library_settings_description": "외부 라이브러리 변경 감시 및 스캔 주기를 설정합니다.", + "library_settings_description": "외부 라이브러리 스캔 및 감시 기능을 설정합니다.", "library_tasks_description": "외부 라이브러리에서 새 항목 또는 변경된 항목을 스캔", "library_watching_enable_description": "외부 라이브러리 파일 변경 감시", "library_watching_settings": "라이브러리 감시 (실험적)", @@ -123,6 +124,13 @@ "logging_enable_description": "로그 기록 활성화", "logging_level_description": "활성화 시 사용할 로그 레벨을 선택합니다.", "logging_settings": "로깅", + "machine_learning_availability_checks": "가용성 확인", + "machine_learning_availability_checks_description": "사용 가능한 머신 러닝 서버를 자동으로 감지하고 우선적으로 선택합니다", + "machine_learning_availability_checks_enabled": "가용성 확인 활성화", + "machine_learning_availability_checks_interval": "확인 주기", + "machine_learning_availability_checks_interval_description": "가용성 확인 주기 (밀리초 단위)", + "machine_learning_availability_checks_timeout": "요청 타임아웃", + "machine_learning_availability_checks_timeout_description": "가용성 확인 요청 타임아웃 (밀리초 단위)", "machine_learning_clip_model": "CLIP 모델", "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 한국어 등 여러 언어로 검색하려면 Multilingual CLIP 모델을 선택하세요. 모델을 변경한 경우 모든 이미지의 '스마트 검색' 작업을 다시 실행해야 합니다.", "machine_learning_duplicate_detection": "비슷한 항목 감지", @@ -146,7 +154,7 @@ "machine_learning_min_recognized_faces": "최소 인식 얼굴", "machine_learning_min_recognized_faces_description": "인물을 생성하기 위해 인식할 얼굴 수의 최솟값을 설정합니다. 값이 높으면 얼굴 인식이 정확해지지만 감지된 얼굴이 인물에 할당되지 않을 가능성이 증가합니다.", "machine_learning_settings": "기계 학습 설정", - "machine_learning_settings_description": "기계 학습에 사용할 모델 및 세부 설정을 관리합니다.", + "machine_learning_settings_description": "기계 학습 시 사용할 모델과 세부 설정을 관리합니다.", "machine_learning_smart_search": "스마트 검색", "machine_learning_smart_search_description": "CLIP 임베딩으로 의미 기반 검색을 사용합니다.", "machine_learning_smart_search_enabled": "스마트 검색 활성화", @@ -157,7 +165,7 @@ "map_dark_style": "다크 스타일", "map_enable_description": "지도 기능 활성화", "map_gps_settings": "지도 및 GPS 설정", - "map_gps_settings_description": "지도를 사용자 정의하거나 역지오코딩 사용 여부를 관리합니다.", + "map_gps_settings_description": "지도 사용자 정의 및 역지오코딩 설정을 관리합니다.", "map_implications": "지도 기능은 외부 타일 서비스(tiles.immich.cloud)에 의존합니다.", "map_light_style": "라이트 스타일", "map_manage_reverse_geocoding_settings": "역지오코딩 설정을 관리합니다.", @@ -174,23 +182,23 @@ "metadata_faces_import_setting": "얼굴 가져오기 활성화", "metadata_faces_import_setting_description": "사이드카 파일의 이미지 EXIF 데이터에서 얼굴 가져오기", "metadata_settings": "메타데이터 설정", - "metadata_settings_description": "메타데이터 사용 설정을 관리합니다.", + "metadata_settings_description": "메타데이터 설정을 관리합니다.", "migration_job": "마이그레이션", - "migration_job_description": "각 항목의 섬네일 및 인물의 얼굴을 최신 폴더 구조로 마이그레이션", - "nightly_tasks_cluster_faces_setting_description": "새로 감지된 얼굴에 대하여 얼굴 인식을 실행", - "nightly_tasks_cluster_new_faces_setting": "새 얼굴 묶기", - "nightly_tasks_database_cleanup_setting": "데이터베이스 정리 작업", - "nightly_tasks_database_cleanup_setting_description": "데이터베이스에서 오래되었거나 만료된 데이터 정리", - "nightly_tasks_generate_memories_setting": "메모리 생성", - "nightly_tasks_generate_memories_setting_description": "항목에서 새로운 메모리 만들기", + "migration_job_description": "각 항목의 섬네일 및 인물의 얼굴을 최신 폴더 구조에 맞게 마이그레이션", + "nightly_tasks_cluster_faces_setting_description": "새로 감지된 얼굴의 인식 작업을 실행합니다.", + "nightly_tasks_cluster_new_faces_setting": "새 얼굴 그룹화", + "nightly_tasks_database_cleanup_setting": "데이터베이스 정리", + "nightly_tasks_database_cleanup_setting_description": "데이터베이스에서 오래되었거나 불필요한 데이터를 정리합니다.", + "nightly_tasks_generate_memories_setting": "추억 생성", + "nightly_tasks_generate_memories_setting_description": "사진 및 동영상에서 새로운 추억 모음을 생성합니다.", "nightly_tasks_missing_thumbnails_setting": "누락된 섬네일 생성", - "nightly_tasks_missing_thumbnails_setting_description": "섬네일이 없는 항목을 섬네일 생성 대기열에 추가", - "nightly_tasks_settings": "야간 작업 설정", - "nightly_tasks_settings_description": "야간에 수행되는 작업을 관리합니다.", + "nightly_tasks_missing_thumbnails_setting_description": "섬네일 생성 대기열에 누락된 항목을 추가합니다.", + "nightly_tasks_settings": "정기 작업 설정", + "nightly_tasks_settings_description": "특정 시간에 수행되는 작업을 관리합니다.", "nightly_tasks_start_time_setting": "시작 시간", - "nightly_tasks_start_time_setting_description": "서버가 야간 작업을 시작할 시간", + "nightly_tasks_start_time_setting_description": "서버가 작업을 시작하는 시간", "nightly_tasks_sync_quota_usage_setting": "사용량 동기화", - "nightly_tasks_sync_quota_usage_setting_description": "현재 사용량 기반으로 사용자의 저장 공간 할당량 업데이트", + "nightly_tasks_sync_quota_usage_setting_description": "사용자의 저장 공간 할당량을 현재 사용량 기반으로 갱신합니다.", "no_paths_added": "추가된 경로 없음", "no_pattern_added": "추가된 규칙 없음", "note_apply_storage_label_previous_assets": "참고: 이전에 업로드한 항목에도 스토리지 레이블을 적용하려면 다음을 실행합니다,", @@ -221,14 +229,14 @@ "oauth_mobile_redirect_uri": "모바일 리다이렉트 URI", "oauth_mobile_redirect_uri_override": "모바일 리다이렉트 URI 오버라이드", "oauth_mobile_redirect_uri_override_description": "OAuth 공급자가 ''{callback}'' 등의 모바일 URI를 허용하지 않는 경우 활성화하세요.", - "oauth_role_claim": "역할 선택", - "oauth_role_claim_description": "요청한 항목을 사용자의 역할로 자동 설정합니다. '사용자' 또는 '관리자'를 선택할 수 있습니다.", + "oauth_role_claim": "역할 클레임", + "oauth_role_claim_description": "요청한 클레임을 사용자의 역할로 자동 설정합니다. 'user' 또는 'admin'을 선택할 수 있습니다.", "oauth_settings": "OAuth", "oauth_settings_description": "OAuth 로그인 설정을 관리합니다.", "oauth_settings_more_details": "이 기능에 대한 자세한 내용은 문서를 참조하세요.", - "oauth_storage_label_claim": "스토리지 레이블 선택", - "oauth_storage_label_claim_description": "요청한 값을 사용자 스토리지 레이블로 자동 설정합니다.", - "oauth_storage_quota_claim": "스토리지 할당량 선택", + "oauth_storage_label_claim": "스토리지 라벨 클레임", + "oauth_storage_label_claim_description": "클레임의 값을 사용자 스토리지 레이블로 자동 설정합니다.", + "oauth_storage_quota_claim": "스토리지 용량 클레임", "oauth_storage_quota_claim_description": "요청한 값을 사용자 스토리지 할당량으로 자동 설정합니다.", "oauth_storage_quota_default": "기본 스토리지 할당량 (GiB)", "oauth_storage_quota_default_description": "입력하지 않은 경우 사용할 GiB 단위의 할당량", @@ -274,7 +282,7 @@ "storage_template_onboarding_description_v2": "활성화하면 사용자 정의 템플릿에 따라 파일이 자동으로 분류됩니다. 자세한 내용은 문서를 참조하세요.", "storage_template_path_length": "대략적인 경로 길이 제한: {length, number}/{limit, number}", "storage_template_settings": "스토리지 템플릿", - "storage_template_settings_description": "업로드된 항목의 폴더 구조 및 파일명을 관리합니다.", + "storage_template_settings_description": "업로드된 항목의 파일명 및 폴더 구조를 관리합니다.", "storage_template_user_label": "사용자의 스토리지 레이블: {label}", "system_settings": "시스템 설정", "tag_cleanup_job": "태그 정리", @@ -318,7 +326,7 @@ "transcoding_encoding_options": "인코딩 옵션", "transcoding_encoding_options_description": "코덱, 해상도, 품질 및 기타 인코딩 옵션을 설정합니다.", "transcoding_hardware_acceleration": "하드웨어 가속", - "transcoding_hardware_acceleration_description": "(실험적) 트랜스코딩 속도는 빨라지지만 동일 비트레이트에서 품질이 상대적으로 저하될 수 있습니다.", + "transcoding_hardware_acceleration_description": "(실험적) 트랜스코딩 속도가 빨라지지만 동일 비트레이트에서 품질이 상대적으로 저하될 수 있습니다.", "transcoding_hardware_decoding": "하드웨어 디코딩", "transcoding_hardware_decoding_setting_description": "종단간 가속으로 디코딩부터 인코딩까지 전체 과정을 가속합니다. 일부 동영상에서는 작동하지 않을 수 있습니다.", "transcoding_max_b_frames": "최대 B-프레임", @@ -337,7 +345,7 @@ "transcoding_reference_frames": "참조 프레임 수", "transcoding_reference_frames_description": "특정 프레임을 압축할 때 참조할 프레임 수를 설정합니다. 값을 높이면 압축 효율이 향상되지만 인코딩 속도가 느려집니다. 0을 입력하면 자동으로 설정합니다.", "transcoding_required_description": "허용 포맷이 아닌 동영상만 트랜스코딩", - "transcoding_settings": "동영상 트랜스코딩 설정", + "transcoding_settings": "트랜스코딩 설정", "transcoding_settings_description": "트랜스코딩할 동영상 및 처리 방법을 관리합니다.", "transcoding_target_resolution": "목표 해상도", "transcoding_target_resolution_description": "해상도를 높이면 세부 정보가 더 많이 보존되지만, 인코딩 시간이 늘어나고 파일 크기가 커져 앱 반응 속도가 느려질 수 있습니다.", @@ -348,16 +356,16 @@ "transcoding_tone_mapping": "톤 매핑", "transcoding_tone_mapping_description": "HDR 영상을 SDR로 변환할 때 사용할 톤 매핑 알고리즘을 설정합니다. Hable은 디테일, Mobius는 색상, Reinhard는 밝기에 중점을 두며 '비활성화'는 톤 매핑을 사용하지 않습니다.", "transcoding_transcode_policy": "트랜스코드 기준", - "transcoding_transcode_policy_description": "동영상을 트랜스코딩할 기준을 설정합니다. HDR 영상은 트랜스코딩이 비활성화된 경우를 제외하면 항상 트랜스코딩됩니다.", + "transcoding_transcode_policy_description": "동영상을 트랜스코딩할 기준을 설정합니다. HDR 동영상은 항상 트랜스코딩됩니다. (트랜스코딩이 비활성화된 경우 제외)", "transcoding_two_pass_encoding": "2패스 인코딩", "transcoding_two_pass_encoding_setting_description": "2패스 인코딩을 사용해 인코딩 품질을 높입니다. H.264 및 HEVC의 경우 CRF를 무시하고 최대 비트레이트 기반의 비트레이트 범위를 사용합니다. VP9의 경우 최대 비트레이트를 비활성화하면 CRF를 사용할 수 있습니다.", "transcoding_video_codec": "동영상 코덱", - "transcoding_video_codec_description": "VP9는 효율적이고 웹 호환성이 높으나 트랜스코딩이 오래 걸립니다. HEVC 역시 비슷하나 웹 호환성이 낮습니다. H.264는 호환성이 가장 높지만 처리된 파일의 크기가 크고, AV1은 가장 효율적이지만 오래된 기기와의 호환성이 낮습니다.", + "transcoding_video_codec_description": "VP9는 효율적이고 웹 호환성이 높으나 트랜스코딩이 오래 걸립니다. HEVC는 VP9와 비슷하지만 웹 호환성이 낮습니다. H.264는 호환성이 가장 높으나 처리된 파일의 크기가 크고, AV1은 가장 효율적이지만 오래된 기기와의 호환성이 낮습니다.", "trash_enabled_description": "휴지통 기능 활성화", "trash_number_of_days": "휴지통 보관 기간", "trash_number_of_days_description": "항목을 영구적으로 삭제하기 전까지 휴지통에 보관할 기간(일)", "trash_settings": "휴지통 설정", - "trash_settings_description": "휴지통 기능 설정을 관리합니다.", + "trash_settings_description": "휴지통 기능을 설정합니다.", "unlink_all_oauth_accounts": "모든 OAuth 계정 연결 해제", "unlink_all_oauth_accounts_description": "새 공급자로 이전하려면 먼저 모든 OAuth 계정의 연결을 해제해야 합니다.", "unlink_all_oauth_accounts_prompt": "모든 OAuth 계정 연결을 해제하시겠습니까? 각 사용자의 OAuth ID를 초기화하며 되돌릴 수 없습니다.", @@ -379,7 +387,7 @@ "version_check_enabled_description": "버전 확인 활성화", "version_check_implications": "주기적으로 Github에 요청을 보내 새 버전을 확인합니다.", "version_check_settings": "버전 확인", - "version_check_settings_description": "새 버전 알림 기능을 관리합니다.", + "version_check_settings_description": "새 버전 확인 및 알림 기능을 관리합니다.", "video_conversion_job": "동영상 트랜스코드", "video_conversion_job_description": "다양한 브라우저 및 기기와의 호환성을 위한 동영상 트랜스코드" }, @@ -387,8 +395,6 @@ "admin_password": "관리자 비밀번호", "administration": "관리", "advanced": "고급", - "advanced_settings_beta_timeline_subtitle": "새로운 앱 경험 사용해보기", - "advanced_settings_beta_timeline_title": "베타 타임라인", "advanced_settings_enable_alternate_media_filter_subtitle": "이 옵션을 사용하면 동기화 중 미디어를 대체 기준으로 필터링할 수 있습니다. 앱이 모든 앨범을 제대로 감지하지 못할 때만 사용하세요.", "advanced_settings_enable_alternate_media_filter_title": "대체 기기 앨범 동기화 필터 사용 (실험적)", "advanced_settings_log_level_title": "로그 레벨: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "서버 이미지 선호", "advanced_settings_proxy_headers_subtitle": "Immich가 네트워크 요청 시 사용할 프록시 헤더를 정의합니다.", "advanced_settings_proxy_headers_title": "프록시 헤더", + "advanced_settings_readonly_mode_subtitle": "읽기 전용 모드를 활성화하면 여러 이미지 선택, 공유, 캐스트, 삭제 동작이 모두 비활성화됩니다. 메인 화면에서 사용자 프로필을 통해 읽기 전용 모드의 활성 상태를 전환하세요", + "advanced_settings_readonly_mode_title": "읽기 전용 모드", "advanced_settings_self_signed_ssl_subtitle": "서버 엔드포인트의 SSL 인증서 검증을 건너뜁니다. 자체 서명 인증서를 사용하는 경우 활성화하세요.", "advanced_settings_self_signed_ssl_title": "자체 서명된 SSL 인증서 허용", "advanced_settings_sync_remote_deletions_subtitle": "웹에서 삭제하거나 복원한 항목을 이 기기에서도 자동으로 처리하도록 설정", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "{user}님을 앨범에서 제거하시겠습니까?", "album_search_not_found": "검색 결과에 해당하는 앨범이 없습니다.", "album_share_no_users": "이미 모든 사용자와 앨범을 공유했거나 공유할 사용자가 없습니다.", + "album_summary": "앨범 요약", "album_updated": "항목 추가 알림", "album_updated_setting_description": "공유 앨범에 항목이 추가된 경우 이메일 알림 받기", "album_user_left": "{album} 앨범에서 나옴", @@ -439,8 +448,8 @@ "albums": "앨범", "albums_count": "앨범 {count, plural, one {{count, number}개} other {{count, number}개}}", "albums_default_sort_order": "기본 앨범 정렬 순서", - "albums_default_sort_order_description": "새 앨범을 생성할 때 기본적으로 항목을 정렬할 순서.", - "albums_feature_description": "다른 사용자와 공유할 수 있는 항목 모음.", + "albums_default_sort_order_description": "새 앨범 생성 시 적용되는 기본 정렬을 설정합니다.", + "albums_feature_description": "여러 사진과 동영상을 한곳에 모아 둘 수 있습니다.", "albums_on_device_count": "기기의 앨범 ({count}개)", "all": "모두", "all_albums": "모든 앨범", @@ -461,13 +470,14 @@ "app_bar_signout_dialog_title": "로그아웃", "app_settings": "앱 설정", "appears_in": "다음 앨범에 포함됨", + "apply_count": "적용 ({count, number})", "archive": "보관함", "archive_action_prompt": "보관함으로 항목 {count}개 이동됨", "archive_or_unarchive_photo": "보관 처리 또는 해제", "archive_page_no_archived_assets": "보관된 항목 없음", "archive_page_title": "보관함 ({count})", "archive_size": "압축 파일 크기", - "archive_size_description": "다운로드할 압축 파일의 크기 구성 (GiB 단위)", + "archive_size_description": "지정한 크기를 초과하면 여러 개의 파일로 분할됩니다. (GiB 단위)", "archived": "보관함", "archived_count": "보관함으로 항목 {count, plural, other {#개}} 이동됨", "are_these_the_same_person": "동일한 인물인가요?", @@ -493,6 +503,8 @@ "asset_restored_successfully": "항목이 복원되었습니다.", "asset_skipped": "건너뜀", "asset_skipped_in_trash": "휴지통의 항목", + "asset_trashed": "항목 삭제됨", + "asset_troubleshoot": "항목 트러블슈팅", "asset_uploaded": "업로드 완료", "asset_uploading": "업로드 중…", "asset_viewer_settings_subtitle": "갤러리 보기 설정을 관리합니다.", @@ -500,7 +512,7 @@ "assets": "항목", "assets_added_count": "항목 {count, plural, one {#개} other {#개}} 추가됨", "assets_added_to_album_count": "앨범에 항목 {count, plural, one {#개} other {#개}} 추가됨", - "assets_added_to_albums_count": "{assetTotal, plural, one {항목 #개} other {항목 #개}}가 앨범 {albumTotal}개에 추가됨", + "assets_added_to_albums_count": "{albumTotal, plural, one {앨범에} other {앨범 #개에}} {assetTotal, plural, one {항목이} other {항목 #개가}} 추가됨", "assets_cannot_be_added_to_album_count": "{count, plural, one {앨범에 항목을} other {일부 항목을 앨범에}} 추가할 수 없습니다.", "assets_cannot_be_added_to_albums": "{count, plural, one {항목을} other {항목들을}} 어느 앨범에도 추가할 수 없습니다.", "assets_count": "{count, plural, one {#개} other {#개}} 항목", @@ -520,14 +532,16 @@ "assets_trashed_from_server": "서버 항목 {count}개 휴지통으로 이동됨", "assets_were_part_of_album_count": "앨범에 이미 포함된 {count, plural, one {항목} other {항목}}입니다.", "assets_were_part_of_albums_count": "이미 앨범에 포함된 {count, plural, one {항목} other {항목}}입니다.", - "authorized_devices": "인증된 기기", + "authorized_devices": "내 기기", "automatic_endpoint_switching_subtitle": "지정된 Wi-Fi가 사용 가능한 경우 내부망으로 연결하고, 그 외의 경우 다른 연결 방식을 사용합니다.", "automatic_endpoint_switching_title": "자동 URL 전환", "autoplay_slideshow": "슬라이드 쇼 자동 재생", "back": "뒤로", - "back_close_deselect": "뒤로, 닫기, 선택 취소", + "back_close_deselect": "뒤로, 닫기 또는 선택 해제", + "background_backup_running_error": "백그라운드 백업이 현재 진행 중이므로 수동 백업을 시작할 수 없습니다", "background_location_permission": "백그라운드 위치 권한", "background_location_permission_content": "Immich가 백그라운드에서 실행 중일 때 네트워크를 전환하려면 Wi-Fi 네트워크 이름을 확인해야 하며, 이를 위해 '정확한 위치' 권한을 항상 허용해야 합니다.", + "background_options": "백그라운드 옵션", "backup": "백업", "backup_album_selection_page_albums_device": "기기의 앨범 ({count})", "backup_album_selection_page_albums_tap": "탭하여 포함, 두 번 탭하여 제외", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "앨범 선택", "backup_album_selection_page_selection_info": "선택한 앨범", "backup_album_selection_page_total_assets": "전체 항목", + "backup_albums_sync": "앨범 동기화 백업", "backup_all": "모두", "backup_background_service_backup_failed_message": "항목 백업에 실패했습니다. 다시 시도하는 중…", "backup_background_service_connection_failed_message": "서버 연결에 실패했습니다. 다시 시도하는 중…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "백그라운드 및 포그라운드 백업 설정을 관리합니다.", "backup_settings_subtitle": "업로드 설정을 관리합니다.", "backward": "뒤로", - "beta_sync": "동기화 (베타) 상태", - "beta_sync_subtitle": "새 동기화 시스템의 설정을 관리합니다.", "biometric_auth_enabled": "생체 인증이 활성화되었습니다.", "biometric_locked_out": "생체 인증이 일시적으로 비활성화되었습니다.", "biometric_no_options": "사용 가능한 생체 인증 옵션 없음", @@ -653,6 +666,8 @@ "change_pin_code": "PIN 코드 변경", "change_your_password": "사용자 계정의 비밀번호를 변경합니다.", "changed_visibility_successfully": "숨김 여부가 변경되었습니다.", + "charging": "충전 중", + "charging_requirement_mobile_backup": "백그라운드 백업은 기기 충전 상태에서 가능합니다", "check_corrupt_asset_backup": "백업된 항목의 손상 여부 확인", "check_corrupt_asset_backup_button": "확인 수행", "check_corrupt_asset_backup_description": "이 검사는 모든 항목이 백업된 후 Wi-Fi가 연결된 상태에서만 실행하세요. 이 작업은 몇 분 정도 소요될 수 있습니다.", @@ -723,14 +738,14 @@ "create": "생성", "create_album": "앨범 생성", "create_album_page_untitled": "제목 없음", - "create_library": "라이브러리 생성", + "create_library": "새 라이브러리", "create_link": "링크 생성", "create_link_to_share": "공유 링크 생성", "create_link_to_share_description": "링크가 있는 경우 누구나 선택한 사진을 볼 수 있습니다.", "create_new": "새로 만들기", "create_new_person": "인물 생성", "create_new_person_hint": "선택한 항목의 인물을 새 인물로 변경", - "create_new_user": "사용자 계정 생성", + "create_new_user": "새 사용자 생성", "create_shared_album_page_share_add_assets": "항목 추가", "create_shared_album_page_share_select_photos": "사진 선택", "create_shared_link": "공유 링크 생성", @@ -739,6 +754,7 @@ "create_user": "사용자 계정 생성", "created": "생성됨", "created_at": "생성됨", + "creating_linked_albums": "링크 연결된 앨범 생성 중...", "crop": "자르기", "curated_object_page_title": "사물", "current_device": "현재 기기", @@ -811,7 +827,7 @@ "display_options": "표시 옵션", "display_order": "표시 순서", "display_original_photos": "원본 사진 표시", - "display_original_photos_setting_description": "원본 사진이 웹과 호환되는 경우 섬네일이 아닌 원본을 표시합니다. 사진이 표시되는 속도가 느려질 수 있습니다.", + "display_original_photos_setting_description": "항목을 표시할 때 웹과 호환되는 경우 원본을 표시합니다. 사진 표시 속도가 느려질 수 있습니다.", "do_not_show_again": "이 메시지를 다시 표시하지 않음", "documentation": "문서", "done": "완료", @@ -823,12 +839,12 @@ "download_error": "다운로드 오류", "download_failed": "다운로드 실패", "download_finished": "다운로드가 완료되었습니다.", - "download_include_embedded_motion_videos": "내장된 동영상", - "download_include_embedded_motion_videos_description": "모션 포토에 내장된 동영상을 개별 파일로 포함", + "download_include_embedded_motion_videos": "모션 포토 영상", + "download_include_embedded_motion_videos_description": "모션 포토에 포함된 동영상을 별도의 파일로 분리해 저장합니다.", "download_notfound": "다운로드할 수 없음", "download_paused": "다운로드 일시 중지됨", "download_settings": "다운로드", - "download_settings_description": "파일 다운로드 방식 및 관련 설정을 관리합니다.", + "download_settings_description": "파일 다운로드 설정을 관리합니다.", "download_started": "다운로드가 시작되었습니다.", "download_sucess": "다운로드가 완료되었습니다.", "download_sucess_android": "항목이 DCIM/Immich 폴더에 다운로드되었습니다.", @@ -888,7 +904,9 @@ "error": "오류", "error_change_sort_album": "앨범 표시 순서 변경 실패", "error_delete_face": "항목에서 얼굴 삭제 중 오류 발생", + "error_getting_places": "장소 정보 입력 실패", "error_loading_image": "이미지를 불러오는 중 오류 발생", + "error_loading_partners": "파트너 불러오기 실패: {error}", "error_saving_image": "오류: {error}", "error_tag_face_bounding_box": "얼굴 태그 실패 - 얼굴의 위치를 가져올 수 없습니다.", "error_title": "오류 - 문제가 발생했습니다", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "즐겨찾기된 항목 없음", "feature_photo_updated": "대표 사진 업데이트됨", "features": "기능", + "features_in_development": "개발 중인 기능", "features_setting_description": "사진 및 동영상 관리 기능을 설정합니다.", "file_name": "파일 이름", "file_name_or_extension": "파일명 또는 확장자", @@ -1060,14 +1079,14 @@ "filetype": "파일 형식", "filter": "필터", "filter_people": "인물 필터", - "filter_places": "장소 필터링", + "filter_places": "장소 필터", "find_them_fast": "이름으로 검색하여 빠르게 찾기", "first": "첫 번째", "fix_incorrect_match": "잘못된 분류 수정", "folder": "폴더", "folder_not_found": "폴더를 찾을 수 없음", "folders": "폴더", - "folders_feature_description": "파일 시스템에 있는 사진과 동영상을 폴더 뷰로 탐색", + "folders_feature_description": "파일 시스템의 사진과 동영상을 폴더 보기로 탐색합니다.", "forgot_pin_code_question": "PIN 번호를 잊어버렸나요?", "forward": "앞으로", "gcast_enabled": "구글 캐스트", @@ -1079,6 +1098,8 @@ "go_back": "뒤로", "go_to_folder": "폴더로 이동", "go_to_search": "검색으로 이동", + "gps": "GPS", + "gps_missing": "GPS 없음", "grant_permission": "권한 부여", "group_albums_by": "다음으로 앨범 그룹화...", "group_country": "국가별로 그룹화", @@ -1182,7 +1203,7 @@ "language_no_results_subtitle": "다른 검색어를 사용해 보세요.", "language_no_results_title": "결과 없음", "language_search_hint": "언어 검색...", - "language_setting_description": "선호하는 언어 선택", + "language_setting_description": "사용할 언어를 선택하세요.", "large_files": "큰 파일", "last": "마지막", "last_seen": "최근 활동", @@ -1214,6 +1235,7 @@ "local": "로컬", "local_asset_cast_failed": "서버에 업로드되지 않은 항목을 캐스팅할 수 없음", "local_assets": "로컬 항목", + "local_media_summary": "로컬 미디어 요약", "local_network": "로컬 네트워크", "local_network_sheet_info": "지정된 Wi-Fi를 사용할 때 앱이 아래 URL로 서버에 연결합니다.", "location_permission": "위치 권한", @@ -1225,6 +1247,7 @@ "location_picker_longitude_hint": "여기에 경도를 입력하세요", "lock": "잠금", "locked_folder": "잠금 폴더", + "log_detail_title": "상세 로그", "log_out": "로그아웃", "log_out_all_devices": "모든 기기에서 로그아웃", "logged_in_as": "{user}로 로그인됨", @@ -1255,18 +1278,20 @@ "login_password_changed_success": "비밀번호가 변경되었습니다.", "logout_all_device_confirmation": "모든 기기에서 로그아웃하시겠습니까?", "logout_this_device_confirmation": "이 기기에서 로그아웃하시겠습니까?", + "logs": "로그", "longitude": "경도", "look": "보기", "loop_videos": "동영상 반복", - "loop_videos_description": "상세 보기에서 자동으로 동영상을 반복 재생합니다.", + "loop_videos_description": "상세 보기에서 영상을 반복 재생합니다.", "main_branch_warning": "개발 버전을 사용 중입니다. 정식 릴리스 버전 사용을 권장합니다!", "main_menu": "메인 메뉴", "make": "제조사", + "manage_geolocation": "위치 정보 관리", "manage_shared_links": "공유 링크 관리", "manage_sharing_with_partners": "공유할 파트너를 초대하거나 제거합니다.", "manage_the_app_settings": "앱 동작 및 표시 환경을 사용자 정의합니다.", "manage_your_account": "사용자의 계정 정보를 확인하고 변경합니다.", - "manage_your_api_keys": "API 키를 생성, 수정 또는 삭제하거나 권한을 관리합니다.", + "manage_your_api_keys": "API 키를 생성, 삭제하거나 권한을 관리합니다.", "manage_your_devices": "현재 로그인한 기기를 확인하고 로그아웃할 수 있습니다.", "manage_your_oauth_connection": "OAuth 연결을 관리합니다.", "map": "지도", @@ -1296,6 +1321,7 @@ "mark_as_read": "읽음으로 표시", "marked_all_as_read": "모두 읽음으로 표시했습니다.", "matches": "일치", + "matching_assets": "일치하는 항목", "media_type": "미디어 종류", "memories": "추억", "memories_all_caught_up": "모두 확인함", @@ -1336,6 +1362,7 @@ "name_or_nickname": "이름 또는 닉네임", "network_requirement_photos_upload": "사진 백업에 모바일 데이터 사용", "network_requirement_videos_upload": "동영상 백업에 모바일 데이터 사용", + "network_requirements": "네트워크 요구사항", "network_requirements_updated": "네트워크 상태가 변경되었습니다. 백업 대기열을 초기화합니다.", "networking_settings": "연결", "networking_subtitle": "서버 엔드포인트 설정을 관리합니다.", @@ -1346,6 +1373,7 @@ "new_person": "새 인물 생성", "new_pin_code": "새 PIN 코드", "new_pin_code_subtitle": "잠금 폴더에 처음 접근하셨습니다. 이곳에 안전하게 접근하기 위한 PIN 코드를 설정하세요.", + "new_timeline": "새 타임라인", "new_user_created": "사용자 계정이 생성되었습니다.", "new_version_available": "새 버전 사용 가능", "newest_first": "최신순", @@ -1448,8 +1476,8 @@ "people_edits_count": "인물 {count, plural, one {#명} other {#명}}이 수정되었습니다.", "people_feature_description": "사진과 동영상을 인물 그룹별로 탐색", "people_sidebar_description": "사이드바에 인물 링크 표시", - "permanent_deletion_warning": "영구적으로 삭제 전 경고", - "permanent_deletion_warning_setting_description": "항목을 영구적으로 삭제할 때 경고 메시지 표시", + "permanent_deletion_warning": "영구 삭제 경고", + "permanent_deletion_warning_setting_description": "항목을 완전히 삭제하기 전 경고 메시지를 표시합니다.", "permanently_delete": "영구 삭제", "permanently_delete_assets_count": "{count, plural, one {항목} other {항목}} 영구 삭제", "permanently_delete_assets_prompt": "{count, plural, one {이 항목을} other {항목 #개를}} 영구적으로 삭제하시겠습니까? {count, plural, one {항목이} other {항목이}} 앨범에 포함된 경우 앨범에서 제거됩니다.", @@ -1492,14 +1520,15 @@ "port": "포트", "preferences_settings_subtitle": "앱 개인 설정을 관리합니다.", "preferences_settings_title": "개인 설정", + "preparing": "준비 중", "preset": "프리셋", "preview": "미리 보기", "previous": "이전", "previous_memory": "이전 추억", - "previous_or_next_day": "다음/이전 날", - "previous_or_next_month": "다음/이전 달", - "previous_or_next_photo": "이전/다음 사진", - "previous_or_next_year": "다음/이전 해", + "previous_or_next_day": "이전/다음 날로", + "previous_or_next_month": "이전/다음 달로", + "previous_or_next_photo": "이전/다음 사진으로", + "previous_or_next_year": "이전/다음 연도로", "primary": "기본", "privacy": "개인정보", "profile": "프로필", @@ -1508,6 +1537,7 @@ "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다.", "profile_drawer_github": "Github", + "profile_drawer_readonly_mode": "읽기 전용 모드 활성화. 유저 아바타 아이콘을 길게 눌러 해제할 수 있습니다.", "profile_drawer_server_out_of_date_major": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_server_out_of_date_minor": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_image_of_user": "{user}님의 프로필 이미지", @@ -1553,6 +1583,9 @@ "rating_description": "상세 정보 패널에 EXIF 등급 태그 표시", "reaction_options": "반응 옵션", "read_changelog": "변경 내역 보기", + "readonly_mode_disabled": "읽기 전용 모드 비활성화", + "readonly_mode_enabled": "읽기 전용 모드 활성화", + "ready_for_upload": "업로드 준비 완료", "reassign": "다시 할당", "reassigned_assets_to_existing_person": "{count, plural, one {항목 #개} other {항목 #개}}를 {name, select, null {기존 인물} other {기존 인물 {name}}}에게 재지정했습니다.", "reassigned_assets_to_new_person": "{count, plural, one {항목 #개} other {항목 #개}}를 새 인물에게 재지정했습니다.", @@ -1577,6 +1610,7 @@ "regenerating_thumbnails": "섬네일을 다시 생성하는 중...", "remote": "원격", "remote_assets": "원격 항목", + "remote_media_summary": "원격 미디어 요약", "remove": "제거", "remove_assets_album_confirmation": "앨범에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?", "remove_assets_shared_link_confirmation": "공유 링크에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?", @@ -1760,7 +1794,7 @@ "setting_notifications_total_progress_subtitle": "전체 업로드 진행률 (완료/총 항목)", "setting_notifications_total_progress_title": "백그라운드 백업 전체 진행률 표시", "setting_video_viewer_looping_title": "반복", - "setting_video_viewer_original_video_subtitle": "서버에서 동영상을 스트리밍할 때, 트랜스코딩된 버전이 있더라도 원본을 재생합니다. 이로 인해 버퍼링이 발생할 수 있습니다. 기기에 있는 동영상은 이 설정과 관계없이 항상 원본 화질로 재생됩니다.", + "setting_video_viewer_original_video_subtitle": "동영상 스트리밍 시 트랜스코딩된 파일 대신 원본을 재생합니다. 재생 시 버퍼링이 발생할 수 있습니다. 로컬에 있는 영상은 항상 원본 화질로 재생됩니다.", "setting_video_viewer_original_video_title": "원본 동영상 강제 사용", "settings": "설정", "settings_require_restart": "설정을 적용하려면 Immich를 다시 시작해야 합니다.", @@ -1841,7 +1875,7 @@ "show_in_timeline_setting_description": "타임라인에 이 사용자의 사진과 동영상을 표시", "show_keyboard_shortcuts": "키보드 단축키 표시", "show_metadata": "메타데이터 표시", - "show_or_hide_info": "정보 표시/숨기기", + "show_or_hide_info": "정보 표시 또는 숨기기", "show_password": "비밀번호 표시", "show_person_options": "인물 옵션 표시", "show_progress_bar": "진행 표시줄 표시", @@ -1850,6 +1884,7 @@ "show_slideshow_transition": "슬라이드 전환 표시", "show_supporter_badge": "서포터 배지", "show_supporter_badge_description": "서포터 배지 표시", + "show_text_search_menu": "텍스트 검색 메뉴 표시", "shuffle": "셔플", "sidebar": "사이드바", "sidebar_display_description": "보기 링크를 사이드바에 표시", @@ -1880,6 +1915,7 @@ "stacktrace": "스택 추적", "start": "시작", "start_date": "시작일", + "start_date_before_end_date": "시작일은 종료일보다 이전이어야 합니다", "state": "지역", "status": "상태", "stop_casting": "캐스팅 중단", @@ -1904,6 +1940,8 @@ "sync_albums_manual_subtitle": "업로드한 모든 동영상과 사진을 선택한 백업 앨범에 동기화", "sync_local": "로컬 동기화", "sync_remote": "원격 동기화", + "sync_status": "동기화 상태", + "sync_status_subtitle": "동기화 시스템 확인 및 관리", "sync_upload_album_setting_subtitle": "선택한 앨범을 Immich에 생성하고 사진 및 동영상 업로드", "tag": "태그", "tag_assets": "항목 태그", @@ -1917,8 +1955,8 @@ "tap_to_run_job": "탭하여 작업 실행", "template": "템플릿", "theme": "테마", - "theme_selection": "테마 설정", - "theme_selection_description": "브라우저의 시스템 설정에 따라 자동으로 라이트/다크 테마 설정", + "theme_selection": "테마 선택", + "theme_selection_description": "시스템의 다크 모드 설정에 따라 테마를 자동으로 적용합니다.", "theme_setting_asset_list_storage_indicator_title": "타일에 서버 동기화 상태 표시", "theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({count})", "theme_setting_colorful_interface_subtitle": "기본 색상을 배경에 적용", @@ -1941,7 +1979,9 @@ "to_change_password": "비밀번호 변경", "to_favorite": "즐겨찾기", "to_login": "로그인", + "to_multi_select": "다중 선택", "to_parent": "상위 항목으로", + "to_select": "선택", "to_trash": "삭제", "toggle_settings": "설정 변경", "total": "전체", @@ -1954,13 +1994,14 @@ "trash_emptied": "휴지통을 비웠습니다.", "trash_no_results_message": "삭제된 사진과 동영상이 여기에 표시됩니다.", "trash_page_delete_all": "모두 삭제", - "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 휴지통의 모든 항목이 Immich에서 영구적으로 제거됩니다.", + "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 휴지통의 모든 항목이 Immich에서 영구적으로 삭제됩니다.", "trash_page_info": "휴지통으로 이동된 항목은 {days}일 후 영구적으로 삭제됩니다.", "trash_page_no_assets": "휴지통이 비어 있음", "trash_page_restore_all": "모두 복원", "trash_page_select_assets_btn": "항목 선택", "trash_page_title": "휴지통 ({count})", "trashed_items_will_be_permanently_deleted_after": "휴지통으로 이동된 항목은 {days, plural, one {#일} other {#일}} 후 영구적으로 삭제됩니다.", + "troubleshoot": "트러블슈팅", "type": "형식", "unable_to_change_pin_code": "PIN 코드를 변경할 수 없음", "unable_to_setup_pin_code": "PIN 코드를 설정할 수 없음", @@ -1991,6 +2032,7 @@ "unstacked_assets_count": "항목 {count, plural, one {#개} other {#개}}의 스택을 풀었습니다.", "untagged": "태그 해제됨", "up_next": "다음", + "update_location_action_prompt": "선택한 {count}개 항목 위치 업데이트:", "updated_at": "업데이트됨", "updated_password": "비밀번호가 변경되었습니다.", "upload": "업로드", @@ -2023,11 +2065,11 @@ "user_pin_code_settings_description": "PIN 코드를 변경하거나 잊어버린 경우 초기화합니다.", "user_privacy": "개인정보", "user_purchase_settings": "구매", - "user_purchase_settings_description": "구매 설정을 관리하고 제품 키를 등록 또는 제거합니다.", + "user_purchase_settings_description": "구매 설정을 관리하고 키를 등록 및 제거합니다.", "user_role_set": "{user}에게 {role} 역할 지정", "user_usage_detail": "사용자 사용량 상세", - "user_usage_stats": "계정 사용량 통계", - "user_usage_stats_description": "사진 및 동영상의 총 개수와 범주별 현황을 확인합니다.", + "user_usage_stats": "사용량 통계", + "user_usage_stats_description": "계정의 전체 및 범주별 사용량을 확인합니다.", "username": "계정명", "users": "사용자", "users_added_to_album_count": "사용자 {count, plural, one {#명} other {#명}}을 앨범에 추가했습니다.", @@ -2041,8 +2083,8 @@ "version_history": "버전 기록", "version_history_item": "{date} {version} 설치", "video": "동영상", - "video_hover_setting": "마우스 오버로 영상 미리보기", - "video_hover_setting_description": "영상 섬네일 위에 마우스를 올리면 미리보기가 재생됩니다. 비활성화해도 재생 아이콘에 마우스를 올려 미리볼 수 있습니다.", + "video_hover_setting": "섬네일 영상 미리보기", + "video_hover_setting_description": "섬네일 위에 마우스를 올리면 미리보기를 재생합니다. 비활성화해도 재생 아이콘에 마우스를 올려 미리볼 수 있습니다.", "videos": "동영상", "videos_count": "동영상 {count, plural, one {#개} other {#개}}", "view": "보기", @@ -2057,6 +2099,7 @@ "view_next_asset": "다음 항목 보기", "view_previous_asset": "이전 항목 보기", "view_qr_code": "QR 코드 보기", + "view_similar_photos": "비슷한 사진 보기", "view_stack": "스택 보기", "view_user": "사용자 보기", "viewer_remove_from_stack": "스택에서 제거", @@ -2075,5 +2118,6 @@ "yes": "네", "you_dont_have_any_shared_links": "공유 링크가 없습니다.", "your_wifi_name": "Wi-Fi 네트워크 이름", - "zoom_image": "이미지 확대" + "zoom_image": "이미지 확대", + "zoom_to_bounds": "화면에 맞춰 확대" } diff --git a/i18n/lt.json b/i18n/lt.json index a35ddfa775..b849d335a4 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -28,13 +28,16 @@ "add_to_album": "Pridėti į albumą", "add_to_album_bottom_sheet_added": "Pridėta į {album}", "add_to_album_bottom_sheet_already_exists": "Jau yra albume {album}", + "add_to_album_toggle": "Perjungti pažymėjimus albumui {album}", + "add_to_albums": "Pridėti į albumus", + "add_to_albums_count": "Pridėti į albumus ({count})", "add_to_shared_album": "Pridėti į bendrinamą albumą", "add_url": "Pridėti URL", "added_to_archive": "Pridėta į archyvą", "added_to_favorites": "Pridėta prie mėgstamiausių", "added_to_favorites_count": "{count, plural, one {# pridėtas} few {# pridėti} other {# pridėta}} prie mėgstamiausių", "admin": { - "add_exclusion_pattern_description": "Pridėti išimčių taisyklęs. Plaikomi simboliai *,**, ir ?. Ignoruoti bet kokius failus bet kuriame aplanke užvadintame \"Raw\", naudokite \"**/RAW/**\". Ignoravimui failų su plėtiniu \".tif\", naudokite \"**/*.tiff\". Aplanko kelio nustatymams, naudokite \"/aplanko/kelias/ignoruoti/**\"", + "add_exclusion_pattern_description": "Pridėti išimčių taisykles. Palaikomi simboliai *,**, ir ?. Ignoruoti bet kokius failus bet kuriame aplanke pavadintame \"Raw\", naudokite \"**/RAW/**\". Ignoravimui failų su plėtiniu \".tif\", naudokite \"**/*.tiff\". Aplanko kelio nustatymams, naudokite \"/aplanko/kelias/ignoruoti/**\".", "admin_user": "Administratorius", "asset_offline_description": "Šis išorinės bibliotekos elementas nebepasiekiamas diske ir buvo perkeltas į šiukšliadėžę. Jei failas buvo perkeltas toje pačioje bibliotekoje, laiko skalėje rasite naują atitinkamą elementą. Jei norite šį elementą atkurti, įsitikinkite, kad Immich gali pasiekti failą žemiau nurodytu adresu, ir suvykdykite bibliotekos skenavimą.", "authentication_settings": "Autentifikavimo nustatymai", @@ -45,6 +48,10 @@ "backup_database": "Sukurti duomenų bazės išklotinę", "backup_database_enable_description": "Įgalinti duomenų bazės išklotinės", "backup_keep_last_amount": "Išsaugomų ankstesnių duomenų bazės išklotinių skaičius", + "backup_onboarding_1_description": "išorinė kopija debesyje arba kitoje fizinėje lokacijoje.", + "backup_onboarding_2_description": "vietinės kopijos kituose prietaisuose. Tai apima pagrindinius failus ir jų vietines kopijas.", + "backup_onboarding_3_description": "viso jūsų duomenų kopijų, įskaitant originalius failus. Tai apima 1 išorinę ir 2 vietines kopijas.", + "backup_onboarding_description": "Jūsų duomenų apsaugojimui rekomenduojama 3-2-1 atsarginės kopijos strategija . Jūs turėtumėte saugoti įkeltų nuotraukų/video bei Immich duomenų bazės kopijas išsamiam atsarginių kopijų sprendimui.", "backup_onboarding_footer": "Daugiau informacijos apie „Immich“ atsarginių kopijų kūrimą rasite dokumentacijoje.", "backup_onboarding_parts_title": "3-2-1 atsarginė kopija apima:", "backup_onboarding_title": "Atsarginės kopijos", @@ -75,7 +82,7 @@ "image_format_description": "WebP sukuria mažesnius failus nei JPEG, tačiau lėčiau juos apdoroja.", "image_fullsize_description": "Pilno dydžio nuotrauka be meta duomenų naudojama priartinus", "image_fullsize_enabled": "Įgalinti pilno dydžio nuotraukų generavimą", - "image_fullsize_enabled_description": "Generuoti viso dydžio vaizdą neinternetui pritaikytiems formatams. Kai įjungta parinktis „Pirmenybė įterptai peržiūrai“, įterptosios peržiūros naudojamos tiesiogiai be konvertavimo. Tai neturi įtakos internetui pritaikytiems formatams, pvz., JPEG", + "image_fullsize_enabled_description": "Generuoti viso dydžio vaizdą naršyklėms nepritaikytiems formatams. Kai įjungta parinktis „Pirmenybė įterptai peržiūrai“, įterptosios peržiūros naudojamos tiesiogiai be konvertavimo. Tai neturi įtakos internetui pritaikytiems formatams, pvz., JPEG.", "image_fullsize_quality_description": "Pilno dydžio nuotraukų kokybė 1-100. Didesnė yra geresnė, tačiau sukuria didesniu failus.", "image_fullsize_title": "Pilno dydžio nuotraukų Nustatymai", "image_prefer_embedded_preview": "Pageidautinai rodyti įterptą peržiūrą", @@ -99,8 +106,8 @@ "job_settings": "Užduočių nustatymai", "job_settings_description": "Keisti užduočių lygiagretumą", "job_status": "Užduočių būsenos", - "jobs_delayed": "{jobCount, plural, other {# delayed}}", - "jobs_failed": "{jobCount, plural, other {# failed}}", + "jobs_delayed": "{jobCount, plural, one {# atidėtas} few {# atidėti} other {# atidėtų}}", + "jobs_failed": "{jobCount, plural, other {# nepavyko}}", "library_created": "Sukurta biblioteka: {library}", "library_deleted": "Biblioteka ištrinta", "library_import_path_description": "Nurodykite aplanką, kurį norite importuoti. Šiame aplanke, įskaitant poaplankius, bus nuskaityti vaizdai ir vaizdo įrašai.", @@ -116,6 +123,13 @@ "logging_enable_description": "Įjungti žurnalo vedimą", "logging_level_description": "Įjungus, kokį žurnalo vedimo lygį naudot.", "logging_settings": "Žurnalo vedimas", + "machine_learning_availability_checks": "Prieinamumo patikrinimai", + "machine_learning_availability_checks_description": "Automatiškai aptikti ir teikti pirmenybę prieinamiems mašininio mokymosi serveriams", + "machine_learning_availability_checks_enabled": "Įjungti prieinamumo patikrinimus", + "machine_learning_availability_checks_interval": "Patikros intervalas", + "machine_learning_availability_checks_interval_description": "Intervalas milisekundėmis tarp prieinamumo patikrinimų", + "machine_learning_availability_checks_timeout": "Užklausos laiko limitas", + "machine_learning_availability_checks_timeout_description": "Laiko limitas milisekundėmis prieinamumo patikrinimams", "machine_learning_clip_model": "CLIP modelis", "machine_learning_clip_model_description": "Pavadinimas CLIP modelio įvardintio here. Dėmesio, keičiant modelį jūs privalote iš naujo paleisti 'Išmaniosios Paieškos' užduotį visiems vaizdams.", "machine_learning_duplicate_detection": "Dublikatų aptikimas", @@ -165,7 +179,7 @@ "metadata_extraction_job": "Metaduomenų nuskaitymas", "metadata_extraction_job_description": "Kiekvieno bibliotekos elemento metaduomenų nuskaitymas, tokių kaip GPS koordinatės, veidai ar rezoliucija", "metadata_faces_import_setting": "Įjungti veidų importą", - "metadata_faces_import_setting_description": "Importuoti veidus iš vaizdo EXIF duomenų ir papildomų failų", + "metadata_faces_import_setting_description": "Importuoti veidus iš vaizdo EXIF duomenų ir susietų failų", "metadata_settings": "Metaduomenų nustatymai", "metadata_settings_description": "Tvarkyti metaduomenų nustatymus", "migration_job": "Tvarkymas", @@ -182,6 +196,8 @@ "nightly_tasks_settings_description": "Valdyti naktines užduotis", "nightly_tasks_start_time_setting": "Pradžios laikas", "nightly_tasks_start_time_setting_description": "Laikas kada serveris pradės vykdyti naktines užduotis", + "nightly_tasks_sync_quota_usage_setting": "Sinchronizuoti kvotos naudojimą", + "nightly_tasks_sync_quota_usage_setting_description": "Atnaujinti vartotojo vietos kvotą remiantis dabartiniu vartojimu", "no_paths_added": "Keliai nepridėti", "no_pattern_added": "Šablonas nepridėtas", "note_apply_storage_label_previous_assets": "Pastaba: norėdami pritaikyti Saugyklos Žymą seniau įkeltiems ištekliams, paleiskite", @@ -255,8 +271,8 @@ "storage_template_date_time_description": "Elemento sukūrimo laiko žymė yra naudojama laiko informacijai", "storage_template_date_time_sample": "Pavyzdinis laikas {date}", "storage_template_enable_description": "Aktyvuoti saugyklos šabloną", - "storage_template_hash_verification_enabled": "Aktyvuoti Hash tikrinimą", - "storage_template_hash_verification_enabled_description": "Aktyvuojamas Hash tikrinimas, neišjungti nebent gerai suprantate galimas pasekmes", + "storage_template_hash_verification_enabled": "Aktyvuoti failo parašo tikrinimą", + "storage_template_hash_verification_enabled_description": "Aktyvuojamas failo parašo tikrinimas, neišjungti nebent gerai suprantate galimas pasekmes", "storage_template_migration": "Saugyklos tvarkymas pagal šabloną", "storage_template_migration_description": "Taikyti dabartinį {template} anksčiau įkeltiems duomenims", "storage_template_migration_info": "Saugyklos tvarkyklė konvertuos visus plėtinius mažosiomis raidėmis. Šablonas bus taikomas tik naujiems duomenims. Taikyti šabloną retroaktyviai anksčiau įkeltiems duomenims, paleiskite šią {job}.", @@ -303,29 +319,72 @@ "transcoding_codecs_learn_more": "Sužinoti daugiau apie naudojamą terminologiją, naudokite FFmpeg dokumentaciją H.264 codec, HEVC codec and VP9 codec.", "transcoding_constant_quality_mode": "Pastovios kokybės režimas", "transcoding_constant_quality_mode_description": "ICQ yra geriau nei CPQ, tačiau ne visi įrenginiai palaiko šį spartinimo būdą. Šis pasirinkimas būtų naudojamas kai nustatytas Kodavimas Pagal Kokybę. NVENC nepalaiko šio pasirinkimo todėl bus ignoruojamas.", + "transcoding_constant_rate_factor": "Pastovaus greičio faktorius (-crf)", "transcoding_constant_rate_factor_description": "Video kokybės lygis. Tipinės reikšmės yra 23 jei H.264, 28 jei HVEC, 31 jei VP9, ir 35 jei AV1. Kuo mažesnis tuo kokybiškesnis tačiau didesni failai.", "transcoding_disabled_description": "Nedaryti perkodavimo, įrašų peržiūra gali neveikti ant kai kūrių sąsajų", + "transcoding_encoding_options": "Užkodavimo parinktys", + "transcoding_encoding_options_description": "Nustatyti kodekus, rezoliuciją, kokybę ir kitas parinktis užkoduojamiems vaizdo įrašams", "transcoding_hardware_acceleration": "Techninės įrangos spartinimas", + "transcoding_hardware_acceleration_description": "Eksperimentinis: greitesnis perkodavimas, bet galimai prastesne kokybe prie tos pačios bitų spartos", "transcoding_hardware_decoding": "Aparatinis dekodavimas", + "transcoding_hardware_decoding_setting_description": "Įgalina visapusišką paspartinimą vietoje tik užkodavimo paspartinimo. Gali neveikti su kai kuriais vaizdo įrašais.", + "transcoding_max_b_frames": "Maksimaliai B-kadrų", + "transcoding_max_b_frames_description": "Didesnės reikšmės pagerina suspaudimo efektyvumą, bet sulėtina užkodavimą. Senesniuose prietaisuose gali būti nepalaikomas aparatinis spartinimas. 0 išjungia B-kadrus, o -1 nustato reikšmę automatiškai.", "transcoding_max_bitrate": "Maksimalus bitų srautas", "transcoding_max_bitrate_description": "Pasirenkant max bitrate galima pasiekti labiau nuspėjamą failų dydį su minimaliais kokybės praradimais. Prie 720p, tipinės reikšmės yra 2600 kbits/s jei BP9 ar HVEC, arba 4500 kbits/s jei H.264. Neveiksnus jei pasirenkamas 0.", + "transcoding_max_keyframe_interval": "Maksimalus raktinio kadro intervalas", + "transcoding_max_keyframe_interval_description": "Nustato maksimalų kadro atstumą tarp raktinių kadrų. Žemesnės reikšmės pablogina suspaudimo efektyvumą, bet pagerina prasukimo laiką ir gali pagerinti greito veiksmo scenų kokybę. 0 - nustato šią reikšmę automatiškai.", + "transcoding_optimal_description": "Vaizdo įrašai aukštesne nei tikslinė rezoliucija arba nepalaikomu formatu", "transcoding_policy": "Transkodavimo politika", + "transcoding_policy_description": "Nustatyti kada vaizdo įrašas bus perkoduotas", + "transcoding_preferred_hardware_device": "Pageidaujamas aparatinės įrangos įrenginys", + "transcoding_preferred_hardware_device_description": "Galioja tik VAAPI ir QSV. Nustato dri mazgą aparatiniam perkodavimui.", + "transcoding_preset_preset": "Iš anksto nustatytas (-preset)", "transcoding_preset_preset_description": "Kompresijos greitis. Siekiant tam tikro bitrate lėtesnis apdorojimas lems mažesnius failų dydžius ir padidins kokybę. VP9 ignoruos greičius virš \"gretesnis\" lygio.", + "transcoding_reference_frames": "Nuorodiniai kadrai", + "transcoding_reference_frames_description": "Kadrų, į kuriuos reikia remtis suspaudžiant duotą kadrą, skaičius. Aukštesnė reikšmė pagerina suspaudimo efektyvumą, bet sulėtina užkodavimą. 0 - nustato reikšmę automatiškai.", + "transcoding_required_description": "Tik nepalaikomo formato vaizdo įrašai", + "transcoding_settings": "Vaizdo įrašų perkodavimo nustatymai", + "transcoding_settings_description": "Valdyti kuriuos vaizdo įrašus perkoduoti ir kaip juos apdoroti", + "transcoding_target_resolution": "Skiriamoji geba", "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": "Laikinas adaptyvus kvantavimas", + "transcoding_temporal_aq_description": "Galioja tik NVENC. Pagerina detalių, mažo judesio scenų kokybę. Gali būti nepalaikoma senesnių įrenginių.", + "transcoding_threads": "Gijos", + "transcoding_threads_description": "Didesnės reikšmės pagreitina kodavimą, bet kol aktyvus palieka mažiau serverio resursų kitoms užduotims. Ši reikšmė negali būti didesnė už procesoriaus branduolių kiekį. Jei reikšmė 0, tai išnaudoja maksimaliai.", + "transcoding_tone_mapping": "Tonų atvaizdavimas", + "transcoding_tone_mapping_description": "Bandoma išsaugoti HDR vaizdo įrašų išvaizdą konvertuojant į SDR. Kiekvienas algoritmas taiko skirtingus kompromisus dėl spalvų, detalių ir šviesumo. Hable išsaugo detales, Mobius išsaugo spalvas, o Reinhard išsaugo šviesumą.", + "transcoding_transcode_policy": "Perkodavimo strategija", + "transcoding_transcode_policy_description": "Strategija, kada vaizdo įrašas turi būti perkoduotas. HDR vaizdo įrašai visada bus perkoduoti (išskyrus jei perkodavimas išjungtas).", + "transcoding_two_pass_encoding": "Dviejų perėjimų užkodavimas", + "transcoding_two_pass_encoding_setting_description": "Perkoduoti su dviem perėjimais, kad gauti geriau užkoduotą vaizdo įrašą. Kai maksimalus bitų srautas įjungtas (veikimui reikalaujamas H.264 ir HVEC), tada naudojamas bitų intervalas remiantis maksimaliu bitų srautu ir ignoruojamas CRF. Su VP9 gali būti naudojamas CRF, jei maksimalus bitų srautas yra išjungtas.", "transcoding_video_codec": "Video kodekas", + "transcoding_video_codec_description": "VP9 turi didelį efektyvumą ir tinklo suderinamumą, bet užtrunka ilgiau perkoduojant. HVEC veikia panašiai, bet turi mažesnį tinklo suderinamumą. H.264 yra plačiai palaikomas ir greitai perkoduojamas, bet kuria didelius failus. AV1 yra efektyviausias kodekas, bet nepalaikomas senesnių prietaisų.", "trash_enabled_description": "Įgalinti šiukšliadėžės funkcijas", "trash_number_of_days": "Dienų skaičius", + "trash_number_of_days_description": "Kiek dienų bus laikomi elementai šiukšliadėžėje prieš galutinai juos ištrinant", "trash_settings": "Šiukšliadėžės nustatymai", "trash_settings_description": "Tvarkyti šiukšliadėžės nustatymus", + "unlink_all_oauth_accounts": "Atsieti visas OAuth paskyras", + "unlink_all_oauth_accounts_description": "Nepamirškite atsieti visas OAuth paskyras prieš migruojant pas naują tiekėją.", + "unlink_all_oauth_accounts_prompt": "Ar tikrai norite atsieti visas OAuth paskyras? Tai negrįžtama operacija kuri atstatys OAuth ID kiekvienam vartotojui.", "user_cleanup_job": "Vartotojų išvalymas", + "user_delete_delay": "{user} paskyra ir elementai bus nustatyti galutiniam ištrynimui už {delay, plural, one {# dienos} other {# dienų}}.", "user_delete_delay_settings": "Ištrynimo delsa", - "user_delete_delay_settings_description": "Skaičius dienų po ištrynimo kuomet vartotojo paskyrą ir susiję duomenys bus negražinamai ištrinti. Vartotojo Trynimo užduotis paleidžiama vidurnaktį ir tikrina kurie vartotojai gali būti trinami. Šio nustatymo pakeitimai bus naudojami sekančio užduoties paleidimo metu.", + "user_delete_delay_settings_description": "Skaičius dienų po ištrynimo kuomet naudotojo paskyra ir susiję duomenys bus negražinamai ištrinti. Naudotojo trynimo užduotis paleidžiama vidurnaktį ir tikrina kurie naudotojai gali būti trinami. Šio nustatymo pakeitimai bus naudojami sekančio užduoties paleidimo metu.", + "user_delete_immediately": "{user} paskyra ir elementai bus nedelsiant įtraukti galutiniam pašalinimui.", + "user_delete_immediately_checkbox": "Ištrinti naudotoją ir elementus nedelsiant", + "user_details": "Naudotojo duomenys", "user_management": "Naudotojų valdymas", "user_password_has_been_reset": "Naudotojo slaptažodis buvo iš naujo nustatytas:", + "user_password_reset_description": "Perduokite laikiną slaptažodį naudotojui ir informuokite, kad pasikeistų slaptažodį pirmo prisijungimo metu.", "user_restore_description": "Naudotojo {user} paskyra bus atkurta.", + "user_restore_scheduled_removal": "Atkurti naudotoją - suplanuotas pašalinimas {date, date, long}", "user_settings": "Naudotojo nustatymai", "user_settings_description": "Valdyti naudotojo nustatymus", "user_successfully_removed": "Naudotojas {email} sėkmingai pašalintas.", + "version_check_enabled_description": "Įgalinti versijų tikrinimą", + "version_check_implications": "Versijų tikrinimas reikalauja periodiškos komunikacijos su github.com", "version_check_settings": "Versijos tikrinimas", "version_check_settings_description": "Įjungti/išjungti naujos versijos pranešimus", "video_conversion_job": "Vaizdo įrašų konvertavimas", @@ -334,10 +393,34 @@ "admin_email": "Administratoriaus el. paštas", "admin_password": "Administratoriaus slaptažodis", "administration": "Administravimas", + "advanced": "Sudėtingesnis", + "advanced_settings_enable_alternate_media_filter_subtitle": "Naudokite šį nustatymą medijos filtravimui sinchronizuojant remiantis alternatyviais kriterijais. Naudokite tik jei programa turi problemų su visų albumų aptikimu.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTINIS] Naudokite alternatyvų įrenginio albumų sinchronizavimo filtrą", + "advanced_settings_log_level_title": "Žurnalo įrašų lygis: {level}", + "advanced_settings_prefer_remote_subtitle": "Kai kurie įrenginiai labai lėtai įkelia miniatiūras iš vietinių elementų. Aktyvuokite šį nustatymą, kad vietoje to užkrautumėte nuotolines nuotraukas.", + "advanced_settings_prefer_remote_title": "Teikti pirmenybę nuotolinėms nuotraukoms", + "advanced_settings_proxy_headers_subtitle": "Nustatykite tarpinio serverio antraštes kurias Immich siųs su kiekvienu užklausimu", + "advanced_settings_proxy_headers_title": "Tarpinio serverio antraštės", + "advanced_settings_readonly_mode_subtitle": "Įgalina tik skaitymo režimą kai nuotraukas galima tik žiūrėti, draudžiama pažymėti kelias, dalintis, transliuoti ar ištrinti. Įgalinkit/uždrauskit tik skaitymą per naudotojo avatar'ą iš pagrindinio lango", + "advanced_settings_readonly_mode_title": "Tik skaitymo režimas", + "advanced_settings_self_signed_ssl_subtitle": "Praleidžia SSL sertifikato tikrinimą serverio galutiniam taškui. Privaloma pačių pasirašytiems sertifikatams.", + "advanced_settings_self_signed_ssl_title": "Leisti pačių pasirašytus SSL sertifikatus", + "advanced_settings_sync_remote_deletions_subtitle": "Automatiškai ištrinti ar atkurti elementus įrenginyje, kai tie veiksmai atliekami naršyklėje", + "advanced_settings_sync_remote_deletions_title": "Sinchronizuoti nuotolinius ištrynimus [EKSPERIMENTINIS]", + "advanced_settings_tile_subtitle": "Pažangesni naudotojų nustatymai", + "advanced_settings_troubleshooting_subtitle": "Įgalinti papildomas galimybes trikčių šalinimui", + "advanced_settings_troubleshooting_title": "Trikčių šalinimas", + "age_months": "Amžius {months, plural, one {# mėnesis} few {# mėnesiai} other {# mėnesių}}", + "age_year_months": "Amžius 1 metai, {months, plural, one {# mėnesis} few {# mėnesiai} other {# mėnesių}}", + "age_years": "{years, plural, other {Amžius #}}", "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_delete_confirmation_description": "Jei šiuo albumu dalijamasi, tai kiti naudotojai jo nebegalės pasiekti.", + "album_deleted": "Albumas ištrintas", + "album_info_card_backup_album_excluded": "neįtrauktas", + "album_info_card_backup_album_included": "įtrauktas", "album_info_updated": "Albumo informacija atnaujinta", "album_leave": "Palikti albumą?", "album_leave_confirmation": "Ar tikrai norite palikti albumą {album}?", @@ -345,16 +428,28 @@ "album_options": "Albumo parinktys", "album_remove_user": "Pašalinti naudotoją?", "album_remove_user_confirmation": "Ar tikrai norite pašalinti naudotoją {user}?", + "album_search_not_found": "Pagal jūsų paiešką albumų nerasta", "album_share_no_users": "Atrodo, kad bendrinate šį albumą su visais naudotojais, arba neturite naudotojų, su kuriais galėtumėte bendrinti.", + "album_summary": "Albumo santrauka", "album_updated": "Albumas atnaujintas", "album_updated_setting_description": "Gauti pranešimą el. paštu, kai bendrinamas albumas turi naujų elementų", + "album_user_left": "Paliko {album}", "album_user_removed": "Pašalintas {user}", + "album_viewer_appbar_delete_confirm": "Ar tikrai norite ištrinti šį albumą iš savo paskyros?", "album_viewer_appbar_share_err_delete": "Nepavyko ištrinti albumo", "album_viewer_appbar_share_err_leave": "Nepavyko išeiti iš albumo", + "album_viewer_appbar_share_err_remove": "Kilo problemų pašalinant elementus iš albumo", "album_viewer_appbar_share_err_title": "Nepavyko pakeisti albumo pavadinimą", + "album_viewer_appbar_share_leave": "Palikti albumą", + "album_viewer_appbar_share_to": "Dalintis su", + "album_viewer_page_share_add_users": "Pridėti naudotojų", "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ų}}", + "albums_default_sort_order": "Pradinė albumo rūšiavimo tvarka", + "albums_default_sort_order_description": "Pradinė elementų rūšiavimo tvarka kai kuriamas naujas albumas.", + "albums_feature_description": "Elementų rinkinys kuriuo galima dalintis su kitais naudotojais.", + "albums_on_device_count": "Albumų įrenginyje ({count})", "all": "Visi", "all_albums": "Visi albumai", "all_people": "Visi žmonės", @@ -363,51 +458,159 @@ "allow_edits": "Leisti redagavimus", "allow_public_user_to_download": "Leisti viešam naudotojui atsisiųsti", "allow_public_user_to_upload": "Leisti viešam naudotojui įkelti", + "alt_text_qr_code": "QR kodo paveiksliukas", + "anti_clockwise": "Prieš laikrodžio rodykles", "api_key": "API raktas", + "api_key_description": "Ši reikšmė bus parodyta tik vieną kartą. Prašome nusikopijuoti prieš uždarant šį langą.", "api_key_empty": "Jūsų API rakto pavadinimas netūrėtų būti tuščias", "api_keys": "API raktai", + "app_bar_signout_dialog_content": "Ar tikrai norite atsijungti?", + "app_bar_signout_dialog_ok": "Taip", + "app_bar_signout_dialog_title": "Atsijungti", "app_settings": "Programos nustatymai", + "appears_in": "Susiję", + "apply_count": "Taikyti ({count, number})", "archive": "Archyvas", + "archive_action_prompt": "{count} pridėta į archyvą", "archive_or_unarchive_photo": "Archyvuoti arba išarchyvuoti nuotrauką", "archive_page_no_archived_assets": "Nerasta jokių archyvuotų elementų", + "archive_page_title": "Archyve ({count})", "archive_size": "Archyvo dydis", "archive_size_description": "Konfigūruoti archyvo dydį atsisiuntimams (GiB)", "archived": "Archyvuota", "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": "Negalima ištrinti tik skaitom(o, ų) element(o, ų), praleidžiama", + "asset_action_share_err_offline": "Negalima užkrauti neprisijungusių elementų, praleidžiama", "asset_added_to_album": "Pridėta į albumą", - "asset_adding_to_album": "Pridedama į albumą...", + "asset_adding_to_album": "Pridedama į albumą…", "asset_description_updated": "Elemento aprašymas buvo atnaujintas", "asset_filename_is_offline": "Elementas {filename} nepasiekiamas", + "asset_has_unassigned_faces": "Elementas turi nepriskirtų veidų", + "asset_hashing": "Kuriami bylų parašai…", + "asset_list_group_by_sub_title": "Grupuoti pagal", + "asset_list_layout_settings_dynamic_layout_title": "Dinaminis išdėstymas", + "asset_list_layout_settings_group_automatically": "Automatiškai", + "asset_list_layout_settings_group_by": "Grupuoti elementus pagal", + "asset_list_layout_settings_group_by_month_day": "Mėnesis + diena", + "asset_list_layout_sub_title": "Išdėstymas", + "asset_list_settings_subtitle": "Nuotraukų tinklelio išdėstymo nustatymai", + "asset_list_settings_title": "Nuotraukų tinklelis", "asset_offline": "Elementas nepasiekiamas", "asset_offline_description": "Šis išorinis elementas neberandamas diske. Dėl pagalbos susisiekite su savo Immich administratoriumi.", + "asset_restored_successfully": "Elementas atkurtas sėkmingai", + "asset_skipped": "Praleista", + "asset_skipped_in_trash": "Šiukšliadėžėje", + "asset_trashed": "Elementai ištrinti", + "asset_troubleshoot": "Elementų trikčių šalinimas", "asset_uploaded": "Įkelta", - "asset_uploading": "Įkeliama...", + "asset_uploading": "Įkeliama…", + "asset_viewer_settings_subtitle": "Tvarkykite savo galerijos peržiūros nustatymus", + "asset_viewer_settings_title": "Elementų peržiūra", "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_albums_count": "Pridėta {assetTotal, plural, one {# elementas} few {# elementai} other {# elementų}} į {albumTotal, plural, one {# albumą} few {# albumus} other {# albumų}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Elementas negali būti pridėtas} few {Elementai negali būti pridėti} other {Elementų negali būti pridėta}} į albumą", + "assets_cannot_be_added_to_albums": "{count, plural, one {Elementas negali būti pridėtas} few {Elementai negali būti pridėti} other {Elementų negali būti pridėta}} į nei vieną albumą", "assets_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}}", + "assets_deleted_permanently": "{count} elementų ištrinta galutinai", + "assets_deleted_permanently_from_server": "{count} elementų ištrinta galutinai iš Immich serverio", + "assets_downloaded_failed": "{count, plural, one {Atsisiųstas # failas - {error} failas nepavyko} few {Atsisiųsti # failai - {error} failai nepavyko} other {Atsisiųsta # failų - {error} failų nepavyko}}", + "assets_downloaded_successfully": "{count, plural, one {Atsisiųstas # failas sėkmingai} few {Atsisiųsti # failai sėkmingai} other {Atsisiųsta # failų sėkmingai}}", "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": "{count} elementų pašalinta galutinai iš jūsų įrenginio", "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": "{count} element(as, ai, ų) atkurta sėkmingai", + "assets_trashed": "{count} element(ai,ų,as) perkelta į šiukšliadėžę", + "assets_trashed_count": "Perkelta į šiukšliadėžę {count, plural, one {# elementas} few {# elementai} other {# elementų}}", + "assets_trashed_from_server": "{count} element(as, ai, ų) perkelta į šiukšliadėžę iš Immich serverio", "assets_were_part_of_album_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}} jau prieš tai buvo albume", + "assets_were_part_of_albums_count": "{count, plural, one {Elementas } few {Elementai} other {Elementų}} jau buvo albumuose", "authorized_devices": "Autorizuoti įrenginiai", + "automatic_endpoint_switching_subtitle": "Prisijungti vietoje per priskirtą Wi-Fi kai įmanoma ir naudoti alternatyvų prisijungimą visur kitur", + "automatic_endpoint_switching_title": "Automatinis URL perjungimas", + "autoplay_slideshow": "Automatiškai rodyti skaidrių demonstraciją", "back": "Atgal", "back_close_deselect": "Atgal, uždaryti arba atžymėti", + "background_backup_running_error": "Vyksta foninis atsarginis kopijavimas, negalima pradėti rankinio kopijavimo", + "background_location_permission": "Foninis vietovės leidimas", + "background_location_permission_content": "Veikiant fone tinklo perjungimui Immich privalo *visada* turėti prieigą prie tikslios vietovės, kad programa galėtų perskaityti Wi-Fi tinklo pavadinimą", + "background_options": "Fono nuostatos", + "backup": "Atsarginė kopija", + "backup_album_selection_page_albums_device": "Albumų įrenginyje ({count})", + "backup_album_selection_page_albums_tap": "Palieskite įtraukti, du kart palieskite neįtraukti", + "backup_album_selection_page_assets_scatter": "Elementai gali išsibarstyti per kelis albumus. Todėl albumai gali būti įtraukti arba neįtraukti per atsarginio kopijavimo procesą.", + "backup_album_selection_page_select_albums": "Pažymėti albumai", + "backup_album_selection_page_selection_info": "Pažymėjimo informacija", + "backup_album_selection_page_total_assets": "Viso unikalių elementų", + "backup_albums_sync": "Atsarginio kopijavimo albumų sinchronizacija", + "backup_all": "Visi", "backup_background_service_backup_failed_message": "Nepavyko sukurti atsarginių kopijų. Bandoma dar kartą…", "backup_background_service_connection_failed_message": "Nepavyko prisijungti prie serverio. Bandoma dar kartą…", "backup_background_service_current_upload_notification": "Įkeliamas {filename}", + "backup_background_service_default_notification": "Ieškoma naujų elementų…", + "backup_background_service_error_title": "Atsarginio kopijavimo klaida", + "backup_background_service_in_progress_notification": "Kuriama elementų atsarginė kopija…", "backup_background_service_upload_failure_notification": "Nepavyko įkelti {filename}", - "backup_controller_page_background_wifi": "Only on WiFi", + "backup_controller_page_albums": "Atsarginės kopijos albumai", + "backup_controller_page_background_app_refresh_disabled_title": "Foninis programos atnaujinimas išjungtas", + "backup_controller_page_background_app_refresh_enable_button_text": "Eiti į nustatymus", + "backup_controller_page_background_battery_info_link": "Parodyk man kaip", + "backup_controller_page_background_battery_info_message": "Norint geriausių foninio atsarginio kopijavimo rezultatų, prašome išjungti akumuliatoriaus optimizavimą ribojantį foninį Immich veikimą.\n\nKadangi tai priklauso nuo įrenginio, prašome susirasti reikiamą informaciją pas įrenginio gamintoją.", + "backup_controller_page_background_battery_info_ok": "OK", + "backup_controller_page_background_battery_info_title": "Akumuliatoriaus optimizavimai", + "backup_controller_page_background_charging": "Tik kol kraunasi", + "backup_controller_page_background_configure_error": "Nepavyko sukonfigūruoti foninių paslaugų", + "backup_controller_page_background_delay": "Atidėti naujų elementų atsarginį kopijavimą: {duration}", + "backup_controller_page_background_description": "Įjunkite fonines paslaugas, kad galėtumėte automatiškai kurti atsargines kopijas neatidarant programos", + "backup_controller_page_background_is_off": "Automatinis atsarginis kopijavimas yra išjungtas", + "backup_controller_page_background_is_on": "Automatinis atsarginis kopijavimas yra įjungtas", + "backup_controller_page_background_turn_off": "Išjungti fonines paslaugas", + "backup_controller_page_background_turn_on": "Įjungti fonines paslaugas", + "backup_controller_page_background_wifi": "Tik su Wi-Fi", + "backup_controller_page_backup": "Atsarginis kopijavimas", + "backup_controller_page_backup_selected": "Pasirinkta: ", + "backup_controller_page_backup_sub": "Perkeltos nuotraukos ir vaizdo įrašai", "backup_controller_page_created": "Sukurta: {date}", + "backup_controller_page_desc_backup": "Įjunkite foninį atsarginį kopijavimą, kad būtų automatiškai perkeliami nauji elementai į serverį kai atidaroma programa.", + "backup_controller_page_excluded": "Neįtraukta: ", + "backup_controller_page_failed": "Nepavyko ({count})", "backup_controller_page_filename": "Failo pavadinimas: {filename}[{size}]", + "backup_controller_page_id": "ID: {id}", + "backup_controller_page_info": "Atsarginio kopijavimo informacija", + "backup_controller_page_none_selected": "Niekas nepasirinkta", + "backup_controller_page_remainder": "Dar liko", + "backup_controller_page_remainder_sub": "Likusios pasirinktos atsarginio kopijavimo nuotraukos ir vaizdo įrašai", "backup_controller_page_server_storage": "Serverio saugykla", + "backup_controller_page_start_backup": "Pradėti atsarginį kopijavimą", + "backup_controller_page_status_off": "Automatinis foninis atsarginis kopijavimas yra išjungtas", + "backup_controller_page_status_on": "Automatinis foninis atsarginis kopijavimas yra įjungtas", "backup_controller_page_storage_format": "{used} iš {total} panaudota", + "backup_controller_page_to_backup": "Albumai kurių atsarginis kopijavimas bus atliktas", + "backup_controller_page_total_sub": "Visos unikalios nuotraukos ir video įrašai iš pažymėtų albumų", + "backup_controller_page_turn_off": "Išjungti foninį atsarginį kopijavimą", + "backup_controller_page_turn_on": "Įjungti foninį atsarginį kopijavimą", "backup_controller_page_uploading_file_info": "Įkeliama failo info", + "backup_err_only_album": "Negalima pašalinti vienintelio albumo", + "backup_info_card_assets": "elementai", + "backup_manual_cancelled": "Atšaukta", "backup_manual_in_progress": "Jau įkeliama, bandykite dar kartą vėliau", + "backup_manual_success": "Pavyko", + "backup_manual_title": "Įkėlimo būklė", + "backup_options": "Atsarginio kopijavimo nustatymai", + "backup_options_page_title": "Atsarginio kopijavimo nustatymai", + "backup_setting_subtitle": "Tvarkyti foninio ir priekinio plano įkėlimo nustatymus", + "backup_settings_subtitle": "Tvarkyti įkėlimo nustatymus", + "backward": "Atgalinis", + "biometric_auth_enabled": "Biometrinis autentifikavimas įgalintas", + "biometric_locked_out": "Jūs esate užblokuotas biometrinio autentifikavimo funkcijai", + "biometric_no_options": "Nėra galimų biometrinių nustatymų", + "biometric_not_available": "Biometrinis autentifikavimas šiame įrenginyje negalimas", "birthdate_saved": "Sėkmingai išsaugota gimimo data", "birthdate_set_description": "Gimimo data naudojama apskaičiuoti asmens amžių nuotraukos darymo metu.", "blurred_background": "Neryškus fonas", @@ -416,42 +619,106 @@ "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_clear_cache_button": "Išvalyti laikiną talpyklą", + "cache_settings_clear_cache_button_title": "Išvalo programos laikiną talpyklą. Tai gali smarkiai paveikti programos greitį, kol bus sukurta nauja laikinoji talpykla.", + "cache_settings_duplicated_assets_clear_button": "IŠVALYTI", + "cache_settings_duplicated_assets_subtitle": "Nuotraukos ir video įrašai kurie yra programos ignoruojamų sąraše", + "cache_settings_duplicated_assets_title": "Sudubliuoti elementai ({count})", + "cache_settings_statistics_album": "Bibliotekos miniatiūros", + "cache_settings_statistics_full": "Pilno dydžio nuotraukos", + "cache_settings_statistics_shared": "Bendrinamų albumų miniatiūros", + "cache_settings_statistics_thumbnail": "Miniatiūros", + "cache_settings_statistics_title": "Laikinos talpyklos naudojimas", + "cache_settings_subtitle": "Valdykite Immich mobiliosios programos laikinosios talpyklos elgesį", + "cache_settings_tile_subtitle": "Valdykite vietinės talpyklos elgesį", + "cache_settings_tile_title": "Vietinė talpykla", + "cache_settings_title": "Laikinosios talpyklos nustatymai", "camera": "Fotoaparatas", "camera_brand": "Fotoaparato prekės ženklas", "camera_model": "Fotoaparato modelis", "cancel": "Atšaukti", "cancel_search": "Atšaukti paiešką", + "canceled": "Atšaukta", + "canceling": "Atšaukiama", "cannot_merge_people": "Negalima sujungti asmenų", + "cannot_undo_this_action": "Jūs negalėsite atkurti po šio veiksmo!", "cannot_update_the_description": "Negalima atnaujinti aprašymo", + "cast": "Transliuoti", + "cast_description": "Valdyti galimas transliavimo kryptis", "change_date": "Pakeisti datą", + "change_description": "Pakeisti aprašymus", + "change_display_order": "Pakeisti atvaizdavimo tvarką", "change_expiration_time": "Pakeisti galiojimo trukmę", "change_location": "Pakeisti vietovę", "change_name": "Pakeisti vardą", + "change_name_successfully": "Vardas pakeistas sėkmingai", "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": "Patvirtinti slaptažodį", + "change_password_form_description": "Labas {name},\n\nTai yra pirmas kartas kai tu prisijungei prie sistemos arba buvo prašymas pakeisti slaptažodį. Prašome įvesti naują slaptažodį žemiau.", + "change_password_form_new_password": "Naujas slaptažodis", + "change_password_form_password_mismatch": "Slaptažodžiai nesutampa", + "change_password_form_reenter_new_password": "Pakartotinai įveskite naują slaptažodį", + "change_pin_code": "Pakeisti PIN kodą", "change_your_password": "Pakeisti slaptažodį", "changed_visibility_successfully": "Matomumas pakeistas sėkmingai", + "charging": "Kraunasi", + "charging_requirement_mobile_backup": "Foninis kopijavimas reikalauja, kad įrenginys būtų prijungtas pakrovimui", + "check_corrupt_asset_backup": "Patikrinti sugadintų elementų atsarginę kopiją", + "check_corrupt_asset_backup_button": "Atlikti patikrinimą", + "check_corrupt_asset_backup_description": "Paleiskite šį patikrinimą tik per Wi-Fi ir tik kai visi elementai buvo perkopijuoti. Ši procedūra užtruks kelias minutes.", "check_logs": "Tikrinti žurnalus", + "choose_matching_people_to_merge": "Pasirinkite atitinkančius žmones sujungimui", "city": "Miestas", "clear": "Išvalyti", "clear_all": "Išvalyti viską", + "clear_all_recent_searches": "Išvalyti visas naujausias paieškas", + "clear_file_cache": "Išvalyti failų laikiną talpyklą", "clear_message": "Išvalyti pranešimą", "clear_value": "Išvalyti reikšmę", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Įveskite slaptažodį", + "client_cert_import": "Importuoti", + "client_cert_import_success_msg": "Kliento sertifikatas yra importuotas", "client_cert_invalid_msg": "Netinkamas sertifikato failas arba neteisingas slaptažodis", + "client_cert_remove_msg": "Kliento sertifikatas yra pašalintas", + "client_cert_subtitle": "Palaikomi tik PKCS12 (.p12, .pfx) formatai. Sertifikato importavimas/pašalinimas galimas tik prieš prisijungimą", + "client_cert_title": "SSL kliento sertifikatas", + "clockwise": "Pagal laikrodžio rodykles", "close": "Uždaryti", "collapse": "Suskleisti", "collapse_all": "Suskleisti viską", + "color": "Spalva", "color_theme": "Temos spalva", "comment_deleted": "Komentaras ištrintas", "comment_options": "Komentarų parinktys", "comments_and_likes": "Komentarai ir patiktukai", "comments_are_disabled": "Komentarai yra išjungti", + "common_create_new_album": "Sukurti naują albumą", + "common_server_error": "Prašome patikrinti tinklo prisijungimą ir įsitikinti, kad serveris pasiekiamas ir programos/serverio versija sutampa.", + "completed": "Užbaigta", "confirm": "Patvirtinti", "confirm_admin_password": "Patvirtinti administratoriaus slaptažodį", + "confirm_delete_face": "Ar tikrai norite ištrinti {name} veidą iš elementų?", "confirm_delete_shared_link": "Ar tikrai norite ištrinti šią bendrinimo nuorodą?", + "confirm_keep_this_delete_others": "Visi kiti elementai iš krūvos bus ištrinti išskyrus šį elementą. Ar tikrai norite tęsti?", + "confirm_new_pin_code": "Patvirtinkite naują PIN kodą", "confirm_password": "Patvirtinti slaptažodį", + "confirm_tag_face": "Ar norite priskirti šį veidą kaip {name}?", + "confirm_tag_face_unnamed": "Ar norite priskirti šį veidą?", + "connected_device": "Prijungtas įrenginys", + "connected_to": "Prisijungta prie", + "contain": "Tilpti", "context": "Kontekstas", "continue": "Tęsti", + "control_bottom_app_bar_create_new_album": "Sukurti naują albumą", + "control_bottom_app_bar_delete_from_immich": "Ištrinti iš Immich", + "control_bottom_app_bar_delete_from_local": "Ištrinti iš įrenginio", + "control_bottom_app_bar_edit_location": "Redaguoti vietovę", + "control_bottom_app_bar_edit_time": "Redaguoti datą ir laiką", + "control_bottom_app_bar_share_link": "Dalintis nuoroda", + "control_bottom_app_bar_share_to": "Dalintis su", + "control_bottom_app_bar_trash_from_immich": "Perkelti į šiukšliadėžę", "copied_image_to_clipboard": "Nuotrauka nukopijuota į iškarpinę.", "copied_to_clipboard": "Nukopijuota į iškapinę!", "copy_error": "Kopijavimo klaida", @@ -462,6 +729,8 @@ "copy_password": "Kopijuoti slaptažodį", "copy_to_clipboard": "Kopijuoti į iškarpinę", "country": "Šalis", + "cover": "Užpildyti", + "covers": "Viršeliai", "create": "Sukurti", "create_album": "Sukurti albumą", "create_album_page_untitled": "Be pavadinimo", @@ -469,57 +738,116 @@ "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": "SUKURTI NAUJĄ", "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": "PRIDĖTI ELEMENTŲ", + "create_shared_album_page_share_select_photos": "Pažymėti nuotraukas", + "create_shared_link": "Sukurti dalijimosi nuorodą", "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", + "created_at": "Sukurta", + "creating_linked_albums": "Kuriami susieti albumai...", + "crop": "Apkirpti", + "curated_object_page_title": "Daiktai", "current_device": "Dabartinis įrenginys", + "current_pin_code": "Dabartinis PIN kodas", + "current_server_address": "Dabartinis serverio adresas", + "custom_locale": "Pasirinktinė vietovė", "custom_locale_description": "Formatuoti datas ir skaičius pagal kalbą ir regioną", + "custom_url": "Pasirinktinis URL", + "daily_title_text_date": "E, MMM dd", + "daily_title_text_date_year": "E, MMM dd, yyyy", + "dark": "Tamsi", + "dark_theme": "Perjungti tamsią temą", "date_after": "Data po", "date_and_time": "Data ir laikas", "date_before": "Data prieš", + "date_format": "E, LLL d, y • h:mm", "date_of_birth_saved": "Gimimo data sėkmingai išsaugota", + "date_range": "Datų intervalas", "day": "Diena", + "days": "Dienų", "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": "Pradinė vietovė", "default_locale_description": "Formatuoti datas ir skaičius pagal jūsų naršyklės lokalę", "delete": "Ištrinti", + "delete_action_confirmation_message": "Ar tikrai norite ištrinti šį elementą? Šis veiksmas perkels elementą į serverio šiukšliadėžę ir paklaus ar norite ištrinti vietiniame įrenginyje", + "delete_action_prompt": "{count} ištrinta", "delete_album": "Ištrinti albumą", "delete_api_key_prompt": "Ar tikrai norite ištrinti šį API raktą?", + "delete_dialog_alert": "Šie elementai bus galutinai ištrinti iš Immich ir iš jūsų įrenginio", + "delete_dialog_alert_local": "Šie elementai bus galutinai pašalinti iš jūsų įrenginio, bet bus prieinami Immich serveryje", + "delete_dialog_alert_local_non_backed_up": "Kai kurie elementai be Immich atsarginės kopijos ir bus galutinai pašalinti iš jūsų įrenginio", + "delete_dialog_alert_remote": "Šie elementai bus galutinai ištrinti iš Immich serverio", + "delete_dialog_ok_force": "Vis tiek ištrinti", + "delete_dialog_title": "Ištrinti galutinai", "delete_duplicates_confirmation": "Ar tikrai norite visam laikui ištrinti šiuos dublikatus?", + "delete_face": "Ištrinti veidą", "delete_key": "Ištrinti raktą", "delete_library": "Ištrinti biblioteką", "delete_link": "Ištrinti nuorodą", + "delete_local_action_prompt": "{count} ištrinti vietiniame įrenginyje", + "delete_local_dialog_ok_backed_up_only": "Ištrinti tik turinčius atsarginę kopiją", + "delete_local_dialog_ok_force": "Vis tiek ištrinti", + "delete_others": "Ištrinti kitus", + "delete_permanently": "Ištrinti galutinai", + "delete_permanently_action_prompt": "{count} ištrinta galutinai", "delete_shared_link": "Ištrinti bendrinimo nuorodą", + "delete_shared_link_dialog_title": "Ištrinti dalijimosi nuorodą", "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", + "deletes_missing_assets": "Ištrinti diske trūkstamus elementus", "description": "Aprašymas", + "description_input_hint_text": "Pridėti aprašymą...", + "description_input_submit_error": "Klaida atnaujinant aprašymą, pasitikrinkite žurnalą norint detalesnės informacijos", + "deselect_all": "Atžymėti visus", "details": "Detalės", "direction": "Kryptis", "disabled": "Išjungta", "disallow_edits": "Neleisti redaguoti", + "discord": "Discord", "discover": "Atrasti", + "discovered_devices": "Aptikti įrenginiai", "dismiss_all_errors": "Nepaisyti visų klaidų", "dismiss_error": "Nepaisyti klaidos", + "display_options": "Atvaizdavimo parinktys", "display_order": "Atvaizdavimo tvarka", "display_original_photos": "Rodyti originalias nuotraukas", + "display_original_photos_setting_description": "Pirmenybė rodyti originalią nuotrauką vietoje miniatiūros kai originalo elementas yra palaikomas naršyklės. Tai gali lemti lėtesnį nuotraukos rodymo greitį.", "do_not_show_again": "Daugiau nerodyti šio pranešimo", "documentation": "Dokumentacija", + "done": "Atlikta", "download": "Atsisiųsti", + "download_action_prompt": "Atsisiunčiami {count} elementai", + "download_canceled": "Atsisiuntimas atšauktas", + "download_complete": "Atsisiuntimas pabaigtas", + "download_enqueue": "Atsisiuntimai įtraukti į eilę", + "download_error": "Atsisiuntimo klaida", "download_failed": "Nepavyko parsisiųsti", + "download_finished": "Atsisiuntimas pabaigtas", + "download_include_embedded_motion_videos": "Įterpti vaizdo įrašai", "download_include_embedded_motion_videos_description": "Pridėti prie judesio nuotraukų įterptus video kaip atskirą failą", + "download_notfound": "Atsisiuntimas nerastas", "download_paused": "Atsisiuntimas pristabdytas", "download_settings": "Atsisiųsti", + "download_settings_description": "Tvarkyti elementų atsisiuntimo nustatymus", + "download_started": "Atsisiuntimas pradėtas", + "download_sucess": "Atsisiuntimas pavyko", + "download_sucess_android": "Medija buvo atsiųsta į DCIM/Immich", + "download_waiting_to_retry": "Laukiama bandymo iš naujo", "downloading": "Siunčiama", "downloading_asset_filename": "Parsisiunčiamas resursas {filename}", + "downloading_media": "Atsisiunčiama medija", "drop_files_to_upload": "Užkelkite failus bet kurioje vietoje kad įkeltumėte", "duplicates": "Dublikatai", "duplicates_description": "Sutvarkykite kiekvieną elementų grupę nurodydami elementus, kurie yra dublikatai (jei tokių yra)", @@ -527,8 +855,14 @@ "edit": "Redaguoti", "edit_album": "Redaguoti albumą", "edit_avatar": "Redaguoti avatarą", + "edit_birthday": "Redaguoti gimtadienį", "edit_date": "Redaguoti datą", "edit_date_and_time": "Redaguoti datą ir laiką", + "edit_date_and_time_action_prompt": "{count} data ir laikas redaguotas", + "edit_date_and_time_by_offset": "Keisti datą pagal poslinkį", + "edit_date_and_time_by_offset_interval": "Naujas datos intervalas: {from} - {to}", + "edit_description": "Redaguoti aprašymą", + "edit_description_prompt": "Prašome pasirinkti naują aprašymą:", "edit_exclusion_pattern": "Redaguoti išimčių šabloną", "edit_faces": "Redaguoti veidus", "edit_import_path": "Redaguoti importavimo kelią", @@ -536,41 +870,81 @@ "edit_key": "Redaguoti raktą", "edit_link": "Redaguoti nuorodą", "edit_location": "Redaguoti vietovę", + "edit_location_action_prompt": "{count} vietovės pakeistos", + "edit_location_dialog_title": "Vietovė", "edit_name": "Redaguoti vardą", "edit_people": "Redaguoti žmones", "edit_tag": "Redaguoti žymą", "edit_title": "Redaguoti antraštę", "edit_user": "Redaguoti naudotoją", "edited": "Redaguota", + "editor": "Redaktorius", + "editor_close_without_save_prompt": "Pakeitimai nebus išsaugoti", + "editor_close_without_save_title": "Uždaryti redaktorių?", + "editor_crop_tool_h2_aspect_ratios": "Vaizdo santykis", + "editor_crop_tool_h2_rotation": "Pasukimas", "email": "El. paštas", + "email_notifications": "El. pašto pranešimai", + "empty_folder": "Šis katalogas yra tuščias", "empty_trash": "Ištuštinti šiukšliadėžę", + "empty_trash_confirmation": "Ar tikrai norite ištuštinti šiukšliadėžę? Tai galutinai pašalins elementus iš Immich.\nJūs negalėsite atkurti šio veiksmo!", "enable": "Įgalinti", + "enable_backup": "Įgalinti atsargines kopijas", + "enable_biometric_auth_description": "Įveskite savo PIN kodą biometrinės autentifikacijos įjungimui", "enabled": "Įgalintas", "end_date": "Pabaigos data", - "enter_wifi_name": "Enter WiFi name", + "enqueued": "Įtraukta į eilę", + "enter_wifi_name": "Įveskite Wi-Fi pavadinimą", + "enter_your_pin_code": "Įveskite savo PIN kodą", "enter_your_pin_code_subtitle": "Įveskite savo PIN kodą, kad pasiektumėte užrakintą aplanką", "error": "Klaida", + "error_change_sort_album": "Nepavyko pakeisti albumo rūšiavimo tvarkos", + "error_delete_face": "Klaida trinant veidą iš elementų", + "error_getting_places": "Klaida gaunant vietoves", "error_loading_image": "Klaida įkeliant vaizdą", + "error_loading_partners": "Klaida užkraunant partnerius: {error}", + "error_saving_image": "Klaida: {error}", + "error_tag_face_bounding_box": "Klaida aprašant veidą - nepavyko gauti veido vietos koordinačių", "error_title": "Klaida - Kažkas nutiko ne taip", "errors": { + "cannot_navigate_next_asset": "Negalima pereiti prie sekančio elemento", + "cannot_navigate_previous_asset": "Negalima pereiti prie buvusio elemento", "cant_apply_changes": "Negalima taikyti pakeitimų", + "cant_change_activity": "Negalima {enabled, select, true {išjungti} other {įjungti}} veiklos", + "cant_change_asset_favorite": "Elementui negalima pakeisti mėgstamiausio", + "cant_change_metadata_assets_count": "Negalima pakeisti {count, plural, one {# elemento} other {# elementų}} metadata", + "cant_get_faces": "Nepavyko gauti veidus", + "cant_get_number_of_comments": "Nepavyko gauti komentarų skaičiaus", + "cant_search_people": "Negalima ieškoti žmonių", + "cant_search_places": "Negalima ieškoti vietovių", "error_adding_assets_to_album": "Klaida pridedant elementus į albumą", "error_adding_users_to_album": "Klaida pridedant naudotojus prie albumo", + "error_deleting_shared_user": "Klaida trinant pasidalintą naudotoją", "error_downloading": "Klaida atsisiunčiant {filename}", "error_hiding_buy_button": "Klaida slepiant pirkimo mygtuką", "error_removing_assets_from_album": "Klaida šalinant elementus iš albumo, patikrinkite konsolę dėl išsamesnės informacijos", + "error_selecting_all_assets": "Klaida pasirenkant visus elementus", "exclusion_pattern_already_exists": "Šis išimčių šablonas jau egzistuoja.", "failed_to_create_album": "Nepavyko sukurti albumo", "failed_to_create_shared_link": "Nepavyko sukurti bendrinimo nuorodos", "failed_to_edit_shared_link": "Nepavyko redaguoti bendrinimo nuorodos", + "failed_to_get_people": "Nepavyko gauti žmonių", + "failed_to_keep_this_delete_others": "Nepavyko palikti šį elementą ir ištrinti kitus elementus", + "failed_to_load_asset": "Nepavyko užkrauti elemento", + "failed_to_load_assets": "Nepavyko užrauti elementų", + "failed_to_load_notifications": "Nepavyko užkrauti pranešimų", "failed_to_load_people": "Nepavyko užkrauti žmonių", "failed_to_remove_product_key": "Nepavyko pašalinti produkto rakto", + "failed_to_reset_pin_code": "Nepavyko atkurti PIN kodo", "failed_to_stack_assets": "Nepavyko sugrupuoti elementų", "failed_to_unstack_assets": "Nepavyko išgrupuoti elementų", + "failed_to_update_notification_status": "Nepavyko atnaujinti pranešimo statuso", "import_path_already_exists": "Šis importavimo kelias jau egzistuoja.", "incorrect_email_or_password": "Neteisingas el. pašto adresas arba slaptažodis", + "paths_validation_failed": "Nepavyko {paths, plural, one {# kelio} other {# kelių}} patvirtinimas", "profile_picture_transparent_pixels": "Profilio nuotrauka negali turėti permatomų pikselių. Prašome priartinti ir/arba perkelkite nuotrauką.", "quota_higher_than_disk_size": "Nustatyta kvota, viršija disko dydį", + "something_went_wrong": "Kažkas nepavyko", "unable_to_add_album_users": "Nepavyksta pridėti naudotojų prie albumo", "unable_to_add_assets_to_shared_link": "Nepavyko į bendrinimo nuorodą pridėti elementų", "unable_to_add_comment": "Nepavyksta pridėti komentaro", @@ -578,11 +952,15 @@ "unable_to_add_import_path": "Nepavyksta pridėti importavimo kelio", "unable_to_add_partners": "Nepavyksta pridėti partnerių", "unable_to_add_remove_archive": "Nepavyko {archived, select, true {ištraukti iš} other {pridėti prie}} arcyhvo", + "unable_to_add_remove_favorites": "Nepavyko {favorite, select, true {įtraukti elemento į mėgstamiausius} other {pašalinti elemento iš mėgstamiausių}}", "unable_to_archive_unarchive": "Nepavyko {archived, select, true {archyvuoti} other {išarchyvuoti}}", "unable_to_change_album_user_role": "Nepavyksta pakeisti albumo naudotojo rolės", "unable_to_change_date": "Negalima pakeisti datos", + "unable_to_change_description": "Nepavyko pakeisti aprašymo", + "unable_to_change_favorite": "Nepavyko pakeisti elementui mėgstamiausio", "unable_to_change_location": "Negalima pakeisti vietos", "unable_to_change_password": "Negalima pakeisti slaptažodžio", + "unable_to_change_visibility": "Nepavyko pakeisti matomumo {count, plural, one {# asmeniui} few {#asmenims} other {# asmenų}}", "unable_to_complete_oauth_login": "Nepavyko prisijungti su OAuth", "unable_to_connect": "Nepavyko prisijungti", "unable_to_copy_to_clipboard": "Negalima kopijuoti į iškarpinę, įsitikinkite, kad prie puslapio prieinate per https", @@ -591,6 +969,8 @@ "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": "Nepavyko ištrinti elemento", + "unable_to_delete_assets": "Klaida trinant elementus", "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", @@ -598,22 +978,37 @@ "unable_to_download_files": "Nepavyksta atsisiųsti failų", "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": "Nepavyko ištrinti šiukšliadėžės", "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_comments_number": "Nepavyko gauti komentarų skaičiaus", "unable_to_get_shared_link": "Nepavyko gauti bendrinimo nuorodos", "unable_to_hide_person": "Nepavyksta paslėpti žmogaus", + "unable_to_link_motion_video": "Nepavyko susieti judesio video", "unable_to_link_oauth_account": "Nepavyko susieti su OAuth paskyra", "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_reassign_assets_existing_person": "Nepavyko priskirti elementų {name, select, null {egzistuojančiam asmeniui} other {{name}}}", + "unable_to_reassign_assets_new_person": "Nepavyko priskirti elementų naujam asmeniui", "unable_to_refresh_user": "Nepavyksta atnaujinti naudotojo", + "unable_to_remove_album_users": "Nepavyko pašalinti naudotojų iš albumo", "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_library": "Nepavyksta pašalinti bibliotekos", "unable_to_remove_partner": "Nepavyksta pašalinti partnerio", "unable_to_remove_reaction": "Nepavyksta pašalinti reakcijos", + "unable_to_reset_password": "Nepavyko atnaujinti slaptažodžio", + "unable_to_reset_pin_code": "Nepavyko atnaujinti PIN kodo", "unable_to_resolve_duplicate": "Nepavyko sutvarkyti dublikatų", + "unable_to_restore_assets": "Nepavyko atstatyti elementų", + "unable_to_restore_trash": "Nepavyko atstatyti iš šiukšliadėžės", + "unable_to_restore_user": "Nepavyko atstatyti naudotojo", + "unable_to_save_album": "Nepavyko išsaugoti albumo", + "unable_to_save_api_key": "Nepavyko išsaugoti API rakto", + "unable_to_save_date_of_birth": "Nepavyko išsaugoti gimimo datos", + "unable_to_save_name": "Nepavyko išsaugoti vardo", "unable_to_save_profile": "Nepavyko išsaugoti profilio", "unable_to_save_settings": "Nepavyksta išsaugoti nustatymų", "unable_to_scan_libraries": "Nepavyksta nuskaityti bibliotekų", @@ -622,39 +1017,105 @@ "unable_to_set_profile_picture": "Nepavyksta nustatyti profilio nuotraukos", "unable_to_submit_job": "Napvyko sukurti užduoties", "unable_to_trash_asset": "Nepavyko perkelti į šiukšliadėžę", + "unable_to_unlink_account": "Nepavyko atsieti paskyrų", + "unable_to_unlink_motion_video": "Nepavyko atsieti judesio video", + "unable_to_update_album_cover": "Nepavyko atnaujinti albumo viršelio", + "unable_to_update_album_info": "Nepavyko atnaujinti albumo informacijos", + "unable_to_update_library": "Nepavyko atnaujinti bibliotekos", + "unable_to_update_location": "Nepavyko atnaujinti vietovės", + "unable_to_update_settings": "Nepavyko atnaujinti nustatymų", + "unable_to_update_timeline_display_status": "Nepavyko atnaujinti laiko juostos rodymo statuso", + "unable_to_update_user": "Nepavyko atnaujinti naudotoją", "unable_to_upload_file": "Nepavyksta įkelti failo" }, + "exif": "Exif", + "exif_bottom_sheet_description": "Pridėti aprašymą...", + "exif_bottom_sheet_description_error": "Klaida atnaujinant aprašymą", + "exif_bottom_sheet_details": "DETALĖS", + "exif_bottom_sheet_location": "VIETOVĖ", + "exif_bottom_sheet_people": "ŽMONĖS", + "exif_bottom_sheet_person_add_person": "Pridėti vardą", "exit_slideshow": "Išeiti iš skaidrių peržiūros", "expand_all": "Išskleisti viską", + "experimental_settings_new_asset_list_subtitle": "Dirbama", + "experimental_settings_new_asset_list_title": "Įgalinti eksperimentinį nuotraukų tinklelį", + "experimental_settings_subtitle": "Naudokite savo pačių rizika!", + "experimental_settings_title": "Eksperimentinis", + "expire_after": "Galiojimas baigiasi", "expired": "Nebegalioja", "expires_date": "Nebegalios už {date}", "explore": "Naršyti", + "explorer": "Naršyklė", "export": "Eksportuoti", "export_as_json": "Eksportuoti kaip JSON", + "export_database": "Eksportuoti duomenų bazę", + "export_database_description": "Eksportuoti SQLite duomenų bazę", "extension": "Plėtinys", "external": "Išorinis", "external_libraries": "Išorinės bibliotekos", - "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", + "external_network": "Išorinis tinklas", + "external_network_sheet_info": "Kai neprisijungta prie pageidaujamo Wi-Fi tinklo, programa jungsis prie serverio per pirmą URL nuorodą, kurią galės pasiekti, pradedant nuo viršaus į apačią", "face_unassigned": "Nepriskirta", "failed": "Įvyko klaida", + "failed_to_authenticate": "Nepavyko autentifikuoti", + "failed_to_load_assets": "Nepavyko įkelti elementų", + "failed_to_load_folder": "Nepavyko įkelti katalogą", "favorite": "Mėgstamiausias", + "favorite_action_prompt": "{count} pridėta prie mėgstamiausių", "favorite_or_unfavorite_photo": "Įtraukti prie arba pašalinti iš mėgstamiausių", "favorites": "Mėgstamiausi", + "favorites_page_no_favorites": "Nerasta mėgstamiausių elementų", + "feature_photo_updated": "Pageidaujama nuotrauka atnaujinta", "features": "Funkcijos", + "features_in_development": "Kūrimo funkcijos", "features_setting_description": "Valdyti aplikacijos funkcijas", "file_name": "Failo pavadinimas", "file_name_or_extension": "Failo pavadinimas arba plėtinys", "filename": "Failopavadinimas", "filetype": "Failo tipas", + "filter": "Filtras", "filter_people": "Filtruoti žmones", + "filter_places": "Filtruoti vietoves", + "find_them_fast": "Raskite greitai paieškoje pagal vardą", + "first": "Pirmas", + "fix_incorrect_match": "Pataisyti neteisingą porą", + "folder": "Katalogas", + "folder_not_found": "Katalogas nerastas", "folders": "Aplankai", "folders_feature_description": "Peržiūrėkite failų sistemoje esančias nuotraukas ir vaizdo įrašus aplankų rodinyje", + "forgot_pin_code_question": "Pamiršote savo PIN?", + "forward": "Pirmyn", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "Kad veiktų, ši funkcija įkelia išorinius „Google“ išteklius.", + "general": "Bendri", + "geolocation_instruction_location": "Paspauskite ant elemento su GPS koordinatėmis norint naudoti tą vietovę arba pasirinkite vietovę tiesiogiai žemėlapyje", "get_help": "Gauti pagalbos", + "get_wifiname_error": "Nepavyko gauti Wi-Fi pavadinimo. Įsitikinkite, kad suteikti būtini leidimai ir esate prisijungę prie Wi-Fi tinklo", + "getting_started": "Pradedama", + "go_back": "Eiti atgal", + "go_to_folder": "Eiti į katalogą", + "go_to_search": "Eiti į paiešką", + "gps": "GPS", + "gps_missing": "Be GPS", + "grant_permission": "Suteikti leidimą", "group_albums_by": "Grupuoti albumus pagal...", + "group_country": "Grupuoti pagal šalis", "group_no": "Negrupuoti", "group_owner": "Grupuoti pagal savininką", + "group_places_by": "Grupuoti vietoves pagal...", "group_year": "Grupuoti pagal metus", + "haptic_feedback_switch": "Įjungti haptinį grįžtamąjį ryšį", + "haptic_feedback_title": "Haptinis grįžtamasis ryšys", "has_quota": "Turi kvotą", + "hash_asset": "Kurti bylos parašą elementui", + "hashed_assets": "Elementai su bylų parašais", + "hashing": "Bylų parašo kūrimas", + "header_settings_add_header_tip": "Pridėti antraštę", + "header_settings_field_validator_msg": "Reikšmė negali būti tuščia", + "header_settings_header_name_input": "Antraštės pavadinimas", + "header_settings_header_value_input": "Antraštės reikšmė", + "headers_settings_tile_subtitle": "Apibrėžkite tarpinio serverio antraštes, kurias programa turėtų siųsti su kiekviena tinklo užklausa", + "headers_settings_tile_title": "Pasirinktinės tarpinio serverio antraštės", "hi_user": "Labas {name} ({email})", "hide_all_people": "Slėpti visus asmenis", "hide_gallery": "Slėpti galeriją", @@ -662,63 +1123,167 @@ "hide_password": "Slėpti slaptažodį", "hide_person": "Slėpti asmenį", "hide_unnamed_people": "Slėpti neįvardintus asmenis", - "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_add_to_album_conflicts": "Pridėta {added} elementų į albumą {album}. {failed} elementai jau yra albume.", + "home_page_add_to_album_err_local": "Kol kas negalima pridėti vietinių elementų į albumus, praleidžiama", + "home_page_add_to_album_success": "Pridėta {added} elementų į albumą {album}.", + "home_page_album_err_partner": "Kol kas negalima pridėti partnerio elementų į albumą, praleidžiama", + "home_page_archive_err_local": "Kol kas negalima archyvuoti vietinių elementų, praleidžiama", + "home_page_archive_err_partner": "Negalima archyvuoti partnerio elementų, praleidžiama", + "home_page_building_timeline": "Kuriama laiko juosta", + "home_page_delete_err_partner": "Negalima ištrinti partnerio elementų, praleidžiama", + "home_page_delete_remote_err_local": "Vietiniai elementai ištrinant nuotolinį pasirinkimą, praleidžiami", + "home_page_favorite_err_local": "Kol kas negalima priskirti mėgstamiausių vietinių elementų, praleidžiama", + "home_page_favorite_err_partner": "Kol kas negalima priskirti mėgstamiausių partnerio elementų, praleidžiama", + "home_page_first_time_notice": "Jei jūs naudojate programą pirmą kartą, tai prašome pasirinkti atsarginės kopijos albumą, kad laiko juosta galėtų tvarkyti albumo nuotraukas ir vaizdo įrašus", "home_page_locked_error_local": "Nepavyko perkelti lokalių failų į užrakintą aplanką, praleidžiama", "home_page_locked_error_partner": "Nepavyko perkelti partnerio failų į užrakintą aplanką, praleidžiama", + "home_page_share_err_local": "Negalima dalinti vietinių elementų per nuorodą, praleidžiama", + "home_page_upload_err_limit": "Galima įkelti tik iki 30 elementų vienu metu, praleidžiama", + "host": "Šeimininkas", "hour": "Valanda", + "hours": "Valandos", + "id": "ID", + "idle": "Laisva", + "ignore_icloud_photos": "Ignoruoti iCloud nuotraukas", + "ignore_icloud_photos_description": "Nuotraukos laikomos iCloud nebus įkeltos į Immich serverį", "image": "Nuotrauka", + "image_alt_text_date": "{isVideo, select, true {Filmuota} other {Fotografuota}} {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Filmuota} other {Fotografuota}} su {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Filmuota} other {Fotografuota}} su {person1} ir {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Filmuota} other {Fotografuota}} {date} su {person1}, {person2} ir{person3}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Filmuota} other {Fotografuota}} {date} su {person1}, {person2} ir {additionalCount, number} kitais", + "image_alt_text_date_place": "{isVideo, select, true {Filmuota} other {Fotografuota}} {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Filmuota} other {Fotografuota}} su {person1} - {city}, {country} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Filmuota} other {Fotografuota}} su {person1} ir {person2} - {city}, {country} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Filmuota} other {Fotografuota}} su {person1}, {person2}, ir {person3} - {city}, {country} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Filmuota} other {Fotografuota}} su {person1}, {person2}, ir {additionalCount, number} kitais - {city}, {country} {date}", + "image_saved_successfully": "Nuotrauka išsaugota", + "image_viewer_page_state_provider_download_started": "Atsisiuntimas pradėtas", + "image_viewer_page_state_provider_download_success": "Atsisiuntimas pavyko", + "image_viewer_page_state_provider_share_error": "Dalinimosi klaida", "immich_logo": "Immich logotipas", + "immich_web_interface": "Immich Web sąsaja", "import_from_json": "Importuoti iš JSON", "import_path": "Importavimo kelias", + "in_albums": "{count, plural, one {# Albume} few {#Albumuose} other {# Albumų}}", "in_archive": "Archyve", "include_archived": "Įtraukti archyvuotus", "include_shared_albums": "Įtraukti bendrinamus albumus", "include_shared_partner_assets": "Įtraukti partnerio pasidalintus elementus", + "individual_share": "Pavienis pasidalinimas", + "individual_shares": "Pavieniai pasidalinimai", "info": "Informacija", "interval": { "day_at_onepm": "Kiekvieną dieną 13:00", + "hours": "Kas{hours, plural, one {valandą} few {#valandas} other {{hours, number} valandų}}", "night_at_midnight": "Kiekvieną vidurnaktį", "night_at_twoam": "Kiekvieną naktį 02:00" }, + "invalid_date": "Netinkama data", + "invalid_date_format": "Netinkamas datos formatas", "invite_people": "Kviesti žmones", "invite_to_album": "Pakviesti į albumą", + "ios_debug_info_fetch_ran_at": "Užkrovimas vyko {dateTime}", + "ios_debug_info_last_sync_at": "Paskutinė sinchronizacija {dateTime}", + "ios_debug_info_no_processes_queued": "Eilėje nėra foninių procesų", "ios_debug_info_no_sync_yet": "Jokia background sync užduotis dar nebuvo paleista", + "ios_debug_info_processes_queued": "{count, plural, one {Eilėje {count} foninis procesas} few {Eilėje {count} foniniai procesai} other {Eilėje {count} foninių procesų}}", + "ios_debug_info_processing_ran_at": "Apdorojimas vyko {dateTime}", "items_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}}", "jobs": "Užduotys", "keep": "Palikti", "keep_all": "Palikti visus", + "keep_this_delete_others": "Išsaugoti šį, kitus ištrinti", + "kept_this_deleted_others": "Išsaugotas šis elementas ir {count, plural, one {ištrintas # elementas} few {ištrinti # elementai} other {ištrinta # elementų}}", "keyboard_shortcuts": "Spartieji klaviatūros klavišai", "language": "Kalba", + "language_no_results_subtitle": "Bandykite pakoreguoti paieškos terminą", + "language_no_results_title": "Kalbos nerastos", + "language_search_hint": "Ieškoti kalbų...", "language_setting_description": "Pasirinkti pageidaujamą kalbą", + "large_files": "Dideli failai", + "last": "Paskutinis", "last_seen": "Paskutinį kartą matytas", "latest_version": "Naujausia versija", "latitude": "Platuma", "leave": "Išeiti", + "leave_album": "Palikti albumą", + "lens_model": "Lęšių modelis", "let_others_respond": "Leisti kitiems reaguoti", "level": "Lygis", "library": "Biblioteka", "library_options": "Bibliotekos pasirinktys", + "library_page_device_albums": "Albumai įrenginyje", + "library_page_new_album": "Naujas albumas", "library_page_sort_asset_count": "Elementų skaičius", "library_page_sort_created": "Kūrimo data", "library_page_sort_last_modified": "Paskutinį kartą modifikuota", "library_page_sort_title": "Albumo pavadinimas", + "licenses": "Licencijos", + "light": "Šviesi", + "like": "Kaip", + "like_deleted": "Kaip ištrintas", + "link_motion_video": "Susieti judesio vaizdo įrašą", "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ų", - "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", + "local": "Vietinis", + "local_asset_cast_failed": "Negalima transliuoti elemento kuris neįkeltas į serverį", + "local_assets": "Vietiniai elementai", + "local_media_summary": "Vietinės medijos santrauka", + "local_network": "Vietinis tinklas", + "local_network_sheet_info": "Programa jungsis prie serverio per šį URL kai naudos pasirinktą Wi-Fi tinklą", + "location_permission": "Vietovės leidimai", + "location_permission_content": "Norint naudoti automatinio persijungimo opciją, Immich reikia tikslios vietovės leidimo, kad galėtų nuskaityti Wi-Fi tinklo pavadinimą", + "location_picker_choose_on_map": "Pasirinkite žemėlapyje", + "location_picker_latitude_error": "Įveskite tinkamą platumą", + "location_picker_latitude_hint": "Įveskite platumą čia", + "location_picker_longitude_error": "Įveskite tinkamą ilgumą", + "location_picker_longitude_hint": "Įveskite ilgumą čia", + "lock": "Užrakinti", "locked_folder": "Užrakintas aplankas", + "log_detail_title": "Žurnalo detalės", "log_out": "Atsijungti", "log_out_all_devices": "Atsijungti iš visų įrenginių", + "logged_in_as": "Prisijungta kaip {user}", "logged_out_all_devices": "Atsijungta iš visų įrenginių", + "logged_out_device": "Atsijungta nuo įrenginio", "login": "Prisijungti", + "login_disabled": "Prisijungimas neįgalintas", + "login_form_api_exception": "API išimtis. Patikrinkite serverio URL ir bandykite dar kartą.", + "login_form_back_button_text": "Atgal", + "login_form_email_hint": "jusupastas@email.com", + "login_form_endpoint_hint": "http://jusu-serverio-ip:port", + "login_form_endpoint_url": "Serverio galutinio taško URL", + "login_form_err_http": "Prašome nurodyti http:// arba https://", + "login_form_err_invalid_email": "Neteisingas el. paštas", + "login_form_err_invalid_url": "Neteisingas URL", + "login_form_err_leading_whitespace": "Pradinis tarpas", + "login_form_err_trailing_whitespace": "Galinis tarpas", + "login_form_failed_get_oauth_server_config": "Klaida prisijungiant su OAuth, patikrinkite serverio URL", + "login_form_failed_get_oauth_server_disable": "Serveryje OAuth funkcija negalima", + "login_form_failed_login": "Klaida prisijungiant, patikrinkite serverio URL, el. paštą ir slaptažodį", + "login_form_handshake_exception": "Įvyko serverio patvirtinimo išimtis. Jei naudojate savarankiškai pasirašytą sertifikatą, nustatymuose įjunkite savarankiškai pasirašyto sertifikato palaikymą.", + "login_form_password_hint": "slaptažodis", + "login_form_save_login": "Likti prisijungus", + "login_form_server_empty": "Įveskite serverio URL.", + "login_form_server_error": "Nepavyko prisijungti prie serverio.", "login_has_been_disabled": "Prisijungimas išjungtas.", + "login_password_changed_error": "Įvyko klaida atnaujinant jūsų slaptažodį", + "login_password_changed_success": "Slaptažodis sėkmingai atnaujintas", "logout_all_device_confirmation": "Ar tikrai norite atsijungti iš visų įrenginių?", "logout_this_device_confirmation": "Ar tikrai norite atsijungti iš šio prietaiso?", + "logs": "Žurnalas", "longitude": "Ilguma", + "look": "Išvaizda", "loop_videos": "Kartoti vaizdo įrašus", + "loop_videos_description": "Įgalinti automatinį vaizdo įrašo rodymą iš naujo detalių peržiūroje.", + "main_branch_warning": "Jūs naudojate kūrėjo versiją, mes stipriai rekomenduojame naudoti galutinę versiją!", + "main_menu": "Pagrindinis meniu", "make": "Gamintojas", + "manage_geolocation": "Tvarkyti vietovę", "manage_shared_links": "Bendrinimo nuorodų tvarkymas", "manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais", "manage_the_app_settings": "Valdyti programos nustatymus", @@ -727,13 +1292,42 @@ "manage_your_devices": "Valdyti prijungtus įrenginius", "manage_your_oauth_connection": "Tvarkyti OAuth prisijungimą", "map": "Žemėlapis", + "map_assets_in_bounds": "{count, plural, =0 {Nuotraukų nėra} one {# nuotrauka} other {# nuotraukos}}", + "map_cannot_get_user_location": "Negalime gauti naudotojo vietovės", + "map_location_dialog_yes": "Taip", + "map_location_picker_page_use_location": "Naudoti šią vietovę", + "map_location_service_disabled_content": "Vietovės servisas turi būti įjungtas, kad rodytų elementus iš dabartinės vietovės. Įjungti vietovės servisą?", + "map_location_service_disabled_title": "Vietovės servisas išjungtas", + "map_marker_for_images": "Žemėlapio žymeklis nuotraukoms yra {city}, {country}", + "map_marker_with_image": "Žemėlapio žymeklis su nuotrauka", + "map_no_location_permission_content": "Reikalingas vietovės leidimas, kad rodytų elementus iš dabartinės vietovės. Ar norite suteikti leidimą?", + "map_no_location_permission_title": "Vietovės leidimas atmestas", "map_settings": "Žemėlapio nustatymai", + "map_settings_dark_mode": "Tamsi tema", + "map_settings_date_range_option_day": "Pastarosios 24 valandos", + "map_settings_date_range_option_days": "Pastarąsias {days} dienas", + "map_settings_date_range_option_year": "Pastarieji metai", + "map_settings_date_range_option_years": "Pastaruosius {years} metus", + "map_settings_dialog_title": "Žemėlapio nustatymai", "map_settings_include_show_archived": "Įtraukti archyvuotus", + "map_settings_include_show_partners": "Pridėti partneriai", + "map_settings_only_show_favorites": "Rodyti tik mėgstamiausius", + "map_settings_theme_settings": "Žemėlapio tema", + "map_zoom_to_see_photos": "Atitolinkite, kad matytumėte nuotraukas", + "mark_all_as_read": "Pažymėti viską kaip perskaitytą", + "mark_as_read": "Pažymėti kaip perskaitytą", + "marked_all_as_read": "Viskas pažymėta kaip perskaityta", "matches": "Atitikmenys", + "matching_assets": "Atitinkantys elementai", "media_type": "Laikmenos tipas", "memories": "Atsiminimai", + "memories_all_caught_up": "Jau viskas peržiūrėta", + "memories_check_back_tomorrow": "Užsukite rytoj, kad pamatytumėte daugiau prisiminimų", "memories_setting_description": "Valdyti tai, ką matote savo prisiminimuose", - "memory": "Atmintis", + "memories_start_over": "Pradėti iš naujo", + "memories_swipe_to_close": "Perbraukite į viršų norėdami uždaryti", + "memory": "Prisiminimai", + "memory_lane_title": "Prisiminimų juosta {title}", "menu": "Meniu", "merge": "Sujungti", "merge_people": "Sujungti asmenis", @@ -743,24 +1337,40 @@ "merged_people_count": "{count, plural, one {Sujungtas # asmuo} few {Sujungti # asmenys} other {Sujungta # asmenų}}", "minimize": "Sumažinti", "minute": "Minutė", + "minutes": "Minutės", "missing": "Trūkstami", "model": "Modelis", "month": "Mėnesis", + "monthly_title_text_date_format": "MMMM y", "more": "Daugiau", + "move": "Perkelti", "move_off_locked_folder": "Ištraukti iš užrakinto aplanko", "move_to_lock_folder_action_prompt": "{count} įkelta į užrakintą aplanką", "move_to_locked_folder": "Įtraukti į užrakintą aplanką", "move_to_locked_folder_confirmation": "Šios nuotraukos ir vaizdo įrašai bus pašalinti iš visų albumų ir bus matomi tik užrakintame aplanke", + "moved_to_archive": "{count, plural, one {# Elementas perkeltas} few {# Elementai perkelti} other {# Elementų perkelta}} į archyvą", + "moved_to_library": "{count, plural, one {# Elementas perkeltas} few {# Elementai perkelti} other {# Elementų perkelta}} į biblioteką", "moved_to_trash": "Perkelta į šiukšliadėžę", + "multiselect_grid_edit_date_time_err_read_only": "Negalima redaguoti tik skaitomo elemento datos, praleidžiama", + "multiselect_grid_edit_gps_err_read_only": "Negalima redaguoti tik skaitomo elemento vietovės, praleidžiama", + "mute_memories": "Užtildyti prisiminimus", "my_albums": "Mano albumai", "name": "Vardas", "name_or_nickname": "Vardas arba slapyvardis", + "network_requirement_photos_upload": "Naudoti mobilų internetą atsarginėms nuotraukų kopijoms", + "network_requirement_videos_upload": "Naudoti mobilų internetą atsarginėms vaizdo įrašų kopijoms", + "network_requirements": "Tinklo reikalavimai", + "network_requirements_updated": "Tinklo reikalavimai pakeisti, atstatoma atsarginio kopijavimo eilė", + "networking_settings": "Tinklai", + "networking_subtitle": "Tvarkyti serverio galutinio taško nustatymus", "never": "Niekada", "new_album": "Naujas albumas", "new_api_key": "Naujas API raktas", "new_password": "Naujas slaptažodis", "new_person": "Naujas asmuo", + "new_pin_code": "Naujas PIN kodas", "new_pin_code_subtitle": "Tai pirmas kartas, kai naudojate užrakinto aplanko funkciją. Nustatykite PIN kodą savo užrakintam aplankui", + "new_timeline": "Nauja laiko juosta", "new_user_created": "Naujas naudotojas sukurtas", "new_version_available": "PRIEINAMA NAUJA VERSIJA", "newest_first": "Pirmiausia naujausi", @@ -772,32 +1382,68 @@ "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": "Nėra rodomų elementų", + "no_cast_devices_found": "Nerasta transliavimo įrenginių", + "no_checksum_local": "Kontrolinė suma nepasiekiama – negalima gauti vietinių elementų", + "no_checksum_remote": "Kontrolinė suma nepasiekiama – negalima gauti nuotolinių elementų", "no_duplicates_found": "Dublikatų nerasta.", + "no_exif_info_available": "Nėra Exif informacijos", "no_explore_results_message": "Įkelkite daugiau nuotraukų ir tyrinėkite savo kolekciją.", + "no_favorites_message": "Pridėti į mėgstamiausius, kad greitai rastum geriausias nuotraukas ir vaizdo įrašus", "no_libraries_message": "Sukurkite išorinę biblioteką nuotraukoms ir vaizdo įrašams peržiūrėti", + "no_local_assets_found": "Nerasta jokių vietinių elementų su šia kontroline suma", "no_locked_photos_message": "Užrakintame aplanke esančios nuotraukos ir vaizdo įrašai yra paslėpti ir nematomi naršant ir ieškant.", "no_name": "Be vardo", - "no_results": "Nerasta", + "no_notifications": "Pranešimų nėra", + "no_people_found": "Ieškomų žmonių nerasta", + "no_places": "Vietovių nėra", + "no_remote_assets_found": "Nerasta jokių nuotolinių elementų su šia kontroline suma", + "no_results": "Rezultatų nerasta", "no_results_description": "Pabandykite sinonimą arba bendresnį raktažodį", + "no_shared_albums_message": "Sukurkite nuotraukų ar vaizdo įrašų albumą dalinimuisi su žmonėmis jūsų tinkle", + "no_uploads_in_progress": "Nėra vykstančių įkėlimų", + "not_available": "Nepasiekiamas", "not_in_any_album": "Nė viename albume", - "note_apply_storage_label_to_previously_uploaded assets": "Pastaba: Priskirti Saugyklos Žymą prie ankčiau įkeltų ištekliu, paleiskite šį", + "not_selected": "Nepasirinkta", + "note_apply_storage_label_to_previously_uploaded assets": "Pastaba: Priskirti Saugyklos Žymą prie anksčiau įkeltų ištekliu, paleiskite šį", "notes": "Pastabos", + "nothing_here_yet": "Kol kas tuščia", + "notification_permission_dialog_content": "Pranešimų įgalinimui eikite į Nustatymus ir pasirinkite Leisti.", + "notification_permission_list_tile_content": "Suteikti leidimą pranešimų įgalinimui.", + "notification_permission_list_tile_enable_button": "Įgalinti pranešimus", + "notification_permission_list_tile_title": "Pranešimų leidimai", "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", + "offset": "Ofsetas", + "ok": "Ok", "oldest_first": "Seniausias pirmas", "on_this_device": "Šiame įrenginyje", + "onboarding": "Įdarbinimas", + "onboarding_locale_description": "Pasirinkite pageidaujamą kalbą. Vėliau ją galėsite pakeisti nustatymuose.", + "onboarding_privacy_description": "Sekančios (neprivalomos) funkcijos remiasi išorinėmis paslaugomis ir gali būti bet kada išjungtos nustatymuose.", + "onboarding_server_welcome_description": "Nustatykime jūsų programą su dažniausiai naudojamais nustatymais.", + "onboarding_theme_description": "Pasirinkite temos spalvą. Vėliau galite pasikeisti ją nustatymuose.", + "onboarding_user_welcome_description": "Pradėkime!", "onboarding_welcome_user": "Sveiki atvykę, {user}", "online": "Prisijungęs", "only_favorites": "Tik mėgstamiausi", + "open": "Atverti", + "open_in_map_view": "Atverti žemėlapio peržiūroje", + "open_in_openstreetmap": "Atverti per OpenStreetMap", "open_the_search_filters": "Atidaryti paieškos filtrus", "options": "Pasirinktys", "or": "arba", + "organize_into_albums": "Sutvarkyti į albumus", + "organize_into_albums_description": "Sukelti egzistuojančias nuotraukas į albumus naudojant dabartinius sinchronizavimo nustatymus", "organize_your_library": "Tvarkykite savo biblioteką", "original": "Originalas", + "other": "Kiti", "other_devices": "Kiti įrenginiai", + "other_entities": "Kiti subjektai", "other_variables": "Kiti kintamieji", "owned": "Nuosavi", "owner": "Savininkas", @@ -805,12 +1451,27 @@ "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} nuotraukos", + "partner_list_view_all": "Žiūrėti viską", + "partner_page_empty_message": "Jūsų nuotraukomis dar nesidalinama su jokiu partneriu.", + "partner_page_no_more_users": "Nėra daugiau naudotojų pridėjimui", + "partner_page_partner_add_failed": "Nepavyko pridėti partnerio", + "partner_page_select_partner": "Pasirinkite partnerį", + "partner_page_shared_to_title": "Dalinamasi su", + "partner_page_stop_sharing_content": "{partner} daugiau nebegalės pasiekti jūsų nuotraukų.", + "partner_sharing": "Dalinimasis su partneriu", "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": "Per {days, plural, one {pastarąją dieną} few {# pastarąsias dienas} other {# pastarųjų dienų}}", + "hours": "Per {hours, plural, one {pastarąją valandą} few{# pastarąsias valandas} other {# pastarųjų valandų}}", + "years": "Per {years, plural, one {pastaruosius metus} few{# pastaruosius metus} other {# pastarųjų metų}}" + }, "path": "Kelias", + "pattern": "Raštas", "pause": "Sustabdyti", "pause_memories": "Pristabdyti atsiminimus", "paused": "Sustabdyta", @@ -819,27 +1480,73 @@ "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": "Ištrynimo visam laikui perspėjimas", + "permanent_deletion_warning_setting_description": "Rodyti perspėjimą kai elementas ištrinamas visam laikui", "permanently_delete": "Ištrinti visam laikui", "permanently_delete_assets_count": "Visam laikui ištrinti {count, plural, one {# elementą} few {# elementus} other {# elementų}}", + "permanently_delete_assets_prompt": "Ar tikrai norite visam laikui ištrinti {count, plural, one {šitą elementą?} few {šituos # elementus?} other {šitų # elementų?}} Tuo pačiu {count, plural, one {jis bus pašalintas} other {jie bus pašalinti}} iš albumo.", + "permanently_deleted_asset": "Visiškai ištrinti elementai", "permanently_deleted_assets_count": "Visam laikui {count, plural, one {ištrintas # elementas} few {ištrinti # elementai} other {ištrinta # elementų}}", + "permission": "Leidimas", + "permission_empty": "Jūsų leidimas neturėtų būti tuščias", + "permission_onboarding_back": "Atgal", + "permission_onboarding_continue_anyway": "Vis tiek tęsti", + "permission_onboarding_get_started": "Pradėkite", + "permission_onboarding_go_to_settings": "Eiti į nustatymus", + "permission_onboarding_permission_denied": "Leidimas nesuteiktas. Norėdami naudoti Immich, suteikite nuotraukų ir vaizdo įrašų leidimus nustatymuose.", + "permission_onboarding_permission_granted": "Leidimas suteiktas! jūs pasiruošę.", + "permission_onboarding_permission_limited": "Leidimai apriboti. Norėdami leisti Immich kurti atsargines kopijas ir tvarkyti visą jūsų galerijos kolekciją, suteikite nuotraukų ir vaizdo įrašų leidimus nustatymuose.", + "permission_onboarding_request": "Immich reikalingas leidimas peržiūrėti jūsų nuotraukas ir vaizdo įrašus.", + "person": "Asmuo", + "person_age_months": "{months, plural, one {# mėnesio} other {# mėnesių}} amžiaus", + "person_age_year_months": "1 metų ir {months, plural, one {# mėnesio} other {# mėnesių}} amžiaus", + "person_age_years": "{years, plural, other {# metų}} amžiaus", + "person_birthdate": "Gimė {date}", + "person_hidden": "{name}{hidden, select, true { (paslėptas)} other {}}", + "photo_shared_all_users": "Panašu, kad savo nuotraukomis pasidalijote su visais naudotojais arba neturite naudotojų, su kuriais galėtumėte jomis pasidalyti.", "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": "Išsirinkite vietovę", + "pin_code_changed_successfully": "PIN kodas pakeistas sėkmingai", + "pin_code_reset_successfully": "PIN kodas sėkmingai atstatytas", + "pin_code_setup_successfully": "PIN kodas sėkmingai nustatytas", + "pin_verification": "PIN kodo patvirtinimas", "place": "Vieta", "places": "Vietos", + "places_count": "{count, plural, one {{count, number} Vieta} few{{count, number} Vietos} other {{count, number} Vietų}}", + "play": "Paleisti", "play_memories": "Leisti atsiminimus", + "play_motion_photo": "Rodyti judančias nuotraukas", + "play_or_pause_video": "Rodyti arba sustabdyti vaizdo įrašą", + "please_auth_to_access": "Prašome patvirtinti prisijungimą", + "port": "Portas", + "preferences_settings_subtitle": "Tvarkyti programos nuostatas", + "preferences_settings_title": "Nuostatos", + "preset": "Šablonas", + "preview": "Peržiūra", + "previous": "Buvęs", + "previous_memory": "Buvęs prisiminimas", + "previous_or_next_day": "Dieną pirmyn/atgal", + "previous_or_next_month": "Mėnesį pirmyn/atgal", + "previous_or_next_photo": "Nuotrauką pirmyn/atgal", + "previous_or_next_year": "Metus pirmyn/atgal", + "primary": "Pirminis", + "privacy": "Privatumas", "profile": "Profilis", "profile_drawer_app_logs": "Logai", "profile_drawer_client_out_of_date_major": "Mobili aplikacija jau pasenusios versijos. Prašome atsinaujinti į paskutinę didžiąją versiją.", "profile_drawer_client_out_of_date_minor": "Mobili aplikacija jau pasenusios versijos. Prašome atsinaujinti į paskutinę mažąją versiją.", "profile_drawer_client_server_up_to_date": "Klientas ir Serveris yra atnaujinti", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Tik skaitymo rėžimas įgalintas. Ilgai paspauskite vartotojo ikoną išėjimui.", "profile_drawer_server_out_of_date_major": "Serveris jau yra pasenusios versijos. Prašome atsinaujinti į paskutinę didžiąją versiją.", "profile_drawer_server_out_of_date_minor": "Serveris jau yra pasenusios versijos. Prašome atsinaujinti į paskutinę mažąją versiją.", "profile_image_of_user": "{user} profilio nuotrauka", "profile_picture_set": "Profilio nuotrauka nustatyta.", "public_album": "Viešas albumas", + "public_share": "Viešas dilinimasis", "purchase_account_info": "Rėmėjas", "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", "purchase_activated_time": "Suaktyvinta {date}", @@ -854,12 +1561,13 @@ "purchase_failed_activation": "Nepavyko suaktyvinti! Patikrinkite el. paštą, ar turite teisingo produkto koda!", "purchase_individual_description_1": "Asmeniui", "purchase_individual_description_2": "Rėmėjo statusas", + "purchase_individual_title": "Asmeninis", "purchase_input_suggestion": "Turite produkto raktą? Įveskite jį žemiau", "purchase_license_subtitle": "Įsigykite „Immich“, kad palaikytumėte tolesnį paslaugos vystymą", "purchase_lifetime_description": "Pirkimas visam gyvenimui", "purchase_option_title": "PIRKIMO PASIRINKIMAS", "purchase_panel_info_1": "„Immich“ kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu kūrėjų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.", - "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų „Immich“ funkcijų. Mes tikime, kad tokie naudotojai kaip jūs palaikys nuolatinį „Immich“ vystymąsi.", + "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų Immich funkcijų. Mes tikime, kad tokie naudotojai kaip jūs palaikys nuolatinį Immich vystymąsi.", "purchase_panel_title": "Palaikykite projektą", "purchase_per_server": "Vienam serveriui", "purchase_per_user": "Vienam naudotojui", @@ -998,7 +1706,11 @@ "setting_image_viewer_preview_title": "Užkrauti peržiūros nuotrauką", "setting_image_viewer_title": "Nuotraukos", "setting_languages_apply": "Pritaikyti", + "setting_notifications_notify_failures_grace_period": "Informuoti apie foninio atsarginio kopijavimo nesėkmes: {duration}", + "setting_notifications_notify_hours": "{count} valandų", + "setting_notifications_notify_minutes": "{count} minučių", "setting_notifications_notify_never": "niekada", + "setting_notifications_notify_seconds": "{count} sekundžių", "setting_notifications_single_progress_subtitle": "Detali įkėlimo progreso informacija kiekvienam elementui", "settings": "Nustatymai", "settings_require_restart": "Prašome perkrauti Immich, siekiant pritaikyti šį nustatymą", @@ -1006,13 +1718,29 @@ "setup_pin_code": "Nustatyti PIN kodą", "share": "Dalintis", "share_add_photos": "Įtraukti nuotraukų", + "share_assets_selected": "{count} pažymėta", "share_dialog_preparing": "Ruošiama...", "share_link": "Bendrinti nuorodą", "shared": "Bendrinami", "shared_by_user": "Bendrina {user}", "shared_by_you": "Bendrinama jūsų", "shared_from_partner": "Nuotraukos iš {partner}", + "shared_intent_upload_button_progress_text": "{current} / {total} Įkelta", "shared_link_clipboard_copied_massage": "Nukopijuota į iškarpinę", + "shared_link_clipboard_text": "Nuoroda: {link}\nSlaptažodis: {password}", + "shared_link_edit_expire_after_option_days": "{count} dienų", + "shared_link_edit_expire_after_option_hours": "{count} valandų", + "shared_link_edit_expire_after_option_minutes": "{count} minučių", + "shared_link_edit_expire_after_option_months": "{count} mėnesių", + "shared_link_edit_expire_after_option_year": "{count} metų", + "shared_link_expires_day": "Galiojimas baigsis už {count} dienos", + "shared_link_expires_days": "Galiojimas baigsis už {count} dienų", + "shared_link_expires_hour": "Galiojimas baigsis už {count} valandos", + "shared_link_expires_hours": "Galiojimas baigsis už {count} valandų", + "shared_link_expires_minute": "Galiojimas baigsis už {count} minutės", + "shared_link_expires_minutes": "Galiojimas baigsis už {count} minučių", + "shared_link_expires_second": "Galiojimas baigsis už {count} sekundės", + "shared_link_expires_seconds": "Galiojimas baigsis už {count} sekundžių", "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šų}}", @@ -1092,6 +1820,7 @@ "template": "Šablonas", "theme": "Tema", "theme_selection": "Temos pasirinkimas", + "theme_setting_asset_list_tiles_per_row_title": "Elementų per eilutę ({count})", "theme_setting_primary_color_title": "Pagrindinė spalva", "theme_setting_system_primary_color_title": "Naudoti sistemos spalvą", "theme_setting_system_theme_switch": "Automatinė (Naudoti sistemos nustatymus)", @@ -1110,8 +1839,10 @@ "trash_no_results_message": "Į šiukšliadėžę perkeltos nuotraukos ir vaizdo įrašai bus rodomi čia.", "trash_page_delete_all": "Ištrinti Visus", "trash_page_empty_trash_dialog_content": "Ar norite ištrinti išmestus elementus? Šie elementai bus visam laikui pašalinti iš Immich", + "trash_page_info": "Šiukšliadėžės elementai bus galutinai ištrinti už {days} dienų", "trash_page_no_assets": "Nėra išmestų elementų", "trash_page_restore_all": "Atkurti Visus", + "trash_page_title": "Šiukšlių ({count})", "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", @@ -1146,7 +1877,8 @@ "upload_success": "Įkėlimas pavyko, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "upload_to_immich": "Įkelti į Immich ({count})", "uploading": "Įkeliama", - "usage": "Naudojymas", + "url": "URL", + "usage": "Naudojimas", "use_biometric": "Naudoti biometriją", "use_current_connection": "naudoti dabartinį ryšį", "user": "Naudotojas", diff --git a/i18n/lv.json b/i18n/lv.json index cd18b4eddf..941d1c59f7 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -23,7 +23,7 @@ "add_partner": "Pievienot partneri", "add_path": "Pievienot ceļu", "add_photos": "Pievienot fotoattēlus", - "add_tag": "Pievienot Atzīmi", + "add_tag": "Pievienot atzīmi", "add_to": "Pievienot…", "add_to_album": "Pievienot albumam", "add_to_album_bottom_sheet_added": "Pievienots {album}", @@ -44,24 +44,25 @@ "authentication_settings_description": "Paroļu, OAuth un citu autentifikācijas iestatījumu pārvaldība", "authentication_settings_disable_all": "Vai tiešām vēlaties atspējot visas pieteikšanās metodes? Pieteikšanās tiks pilnībā atspējota.", "authentication_settings_reenable": "Lai atkārtoti iespējotu, izmantojiet Servera Komandu.", - "background_task_job": "Fona Uzdevumi", - "backup_database": "Izveidot datu bāzes izgāztuvi", - "backup_database_enable_description": "Iespējot datu bāzes izgāztuvi", - "backup_keep_last_amount": "Iepriekšējo izgāztuvju daudzums, kas jāsaglabā", - "backup_onboarding_1_description": "ārpussaites kopēšana mākonī vai citā fiziskā vietā.", - "backup_onboarding_2_description": "lokālas kopijas citās ierīcēs. Šis iekļauj galvenos failus kā arī dublētu kōpiju ar tiem failiem lokāli.", + "background_task_job": "Fona uzdevumi", + "backup_database": "Izveidot datu bāzes izrakstu", + "backup_database_enable_description": "Iespējot datu bāzes izrakstus", + "backup_keep_last_amount": "Iepriekšējo izrakstu daudzums, kas jāsaglabā", + "backup_onboarding_1_description": "ārēja kopija mākonī vai citā fiziskā atrašanās vietā.", + "backup_onboarding_2_description": "vietējās kopijas citās ierīcēs. Tas ietver galvenos failus un šo failu vietējo rezerves kopiju.", "backup_onboarding_title": "Rezerves kopijas", - "backup_settings_description": "Datubāzes dublēšanas iestatījumu pārvaldība", + "backup_settings": "Datubāzes izrakstu iestatījumi", + "backup_settings_description": "Datubāzes izrakstu iestatījumu pārvaldība", "cleared_jobs": "Notīrīti uzdevumi priekš: {job}", "config_set_by_file": "Konfigurāciju pašlaik iestata konfigurācijas fails", "confirm_delete_library": "Vai tiešām vēlaties dzēst {library} bibliotēku?", "confirm_email_below": "Lai apstiprinātu, zemāk ierakstiet “{email}”", - "confirm_reprocess_all_faces": "Vai tiešām vēlaties atkārtoti apstrādāt visas sejas? Tas arī atiestatīs cilvēkus ar vārdiem.", + "confirm_reprocess_all_faces": "Vai tiešām vēlies atkārtoti apstrādāt visas sejas? Tas arī atiestatīs personas ar vārdiem.", "confirm_user_password_reset": "Vai tiešām vēlaties atiestatīt lietotāja {user} paroli?", "create_job": "Izveidot uzdevumu", "cron_expression": "Cron izteiksme", "disable_login": "Atspējot pieteikšanos", - "duplicate_detection_job_description": "Palaidiet mašīnmācīšanos uz failiem, lai noteiktu līdzīgus attēlus. Paļaujas uz viedo meklēšanu", + "duplicate_detection_job_description": "Analizēt failus ar mašīnmācīšanos, lai noteiktu līdzīgus attēlus. Šī funkcija izmanto viedo meklēšanu", "external_library_management": "Ārējo bibliotēku pārvaldība", "face_detection": "Seju noteikšana", "image_format": "Formāts", @@ -156,13 +157,18 @@ "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", + "paths_validated_successfully": "Visi ceļi veiksmīgi pārbaudīti", "person_cleanup_job": "Personu tīrīšana", "quota_size_gib": "Kvotas izmērs (GiB)", "refreshing_all_libraries": "Atsvaidzina visas bibliotēkas", "registration": "Administratora reģistrācija", + "registration_description": "Tā kā tu esi pirmais sistēmas lietotājs, tev tiks piešķirts administratora statuss un tu būsi atbildīgs par administrēšanas uzdevumiem, kā arī par citu lietotāju izveidi.", "require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", + "reset_settings_to_default": "Atjaunot iestatījumus uz noklusējuma vērtībām", + "reset_settings_to_recent_saved": "Atjaunot iestatījumus uz pēdējiem saglabātajiem iestatījumiem", "scanning_library": "Skenē bibliotēku", "search_jobs": "Meklēt uzdevumus…", + "send_welcome_email": "Nosūtīt sveiciena e-pastu", "server_external_domain_settings": "Ārējais domēns", "server_external_domain_settings_description": "Domēns publiski kopīgotajām saitēm, iekļaujot http(s)://", "server_public_users": "Publiski lietotāji", @@ -170,6 +176,7 @@ "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ā.", + "smart_search_job_description": "Analizēt failus ar mašīnmācīšanos lai sagatavotu datus viedajai meklēšanai", "storage_template_date_time_sample": "Laika paraugs {date}", "storage_template_migration": "Krātuves veidņu migrācija", "storage_template_migration_job": "Krātuves veidņu migrācijas uzdevums", @@ -187,6 +194,7 @@ "theme_custom_css_settings_description": "Cascading Style Sheets ļauj pielāgot Immich izskatu.", "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": "Izveidot lielu, mazu un izplūdušu sīktēlu katram failam, kā arī sīktēlu katrai personai", "transcoding_acceleration_api": "Paātrināšanas API", "transcoding_acceleration_nvenc": "NVENC (nepieciešams NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (nepieciešams 7. paaudzes vai jaunāks Intel procesors)", @@ -205,26 +213,28 @@ "trash_number_of_days": "Dienu skaits", "trash_settings": "Atkritnes iestatījumi", "trash_settings_description": "Atkritnes iestatījumu pārvaldība", + "user_delete_delay_settings": "Dzēšanas aizture", "user_delete_delay_settings_description": "Dienu skaits pēc izdzēšanas, kad neatgriezeniski tiks dzēsti lietotāja konti un faili. Lietotāju dzēšanas uzdevums tiek izpildīts pusnaktī un pārbauda, kuri lietotāji ir gatavi dzēšanai. Izmaiņas šajā iestatījumā tiks ņemtas vērā nākamajā izpildes reizē.", + "user_delete_immediately_checkbox": "Ierindot lietotāju un failus tūlītējai dzēšanai", "user_details": "Lietotāja informācija", "user_management": "Lietotāju pārvaldība", "user_password_has_been_reset": "Lietotāja parole ir atiestatīta:", + "user_password_reset_description": "Lūdzu, norādi lietotājam pagaidu paroli un informē viņu, ka nākamajā pieslēgšanās reizē viņam būs jāmaina parole.", "user_restore_description": "{user} konts tiks atjaunots.", "user_restore_scheduled_removal": "Atjaunot lietotāju - plānotā dzēšana {date, date, long}", "user_settings": "Lietotāja iestatījumi", "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": "Versijas pārbaude", + "version_check_settings_description": "Ieslēgt/izslēgt paziņojumus par jaunu versiju" }, "admin_email": "Administratora e-pasts", "admin_password": "Administratora parole", "administration": "Administrēšana", "advanced": "Papildu", - "advanced_settings_beta_timeline_subtitle": "Izmēģini jauno lietotnes pieredzi", - "advanced_settings_beta_timeline_title": "Bēta laika skala", "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_subtitle": "Dažās ierīcēs sīktēli no ierīces atmiņas ielādējas ļ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_title": "Starpniekservera galvenes", "advanced_settings_self_signed_ssl_subtitle": "Izlaiž servera galapunkta SSL sertifikātu verifikāciju. Nepieciešams pašparakstītajiem sertifikātiem.", @@ -238,6 +248,7 @@ "album_added": "Albums pievienots", "album_added_notification_setting_description": "Saņemt e-pasta paziņojumu, kad tevi pievieno kopīgam albumam", "album_cover_updated": "Albuma attēls atjaunināts", + "album_delete_confirmation_description": "Ja šis albums tiek kopīgots, citi lietotāji vairs nevarēs tam piekļūt.", "album_deleted": "Albums dzēsts", "album_info_card_backup_album_excluded": "NEIEKĻAUTS", "album_info_card_backup_album_included": "IEKĻAUTS", @@ -261,9 +272,9 @@ "albums_default_sort_order_description": "Sākotnējā failu kārtošanas secība, veidojot jaunus albumus.", "albums_feature_description": "Failu kolekcijas, kuras var koplietot ar citiem lietotājiem.", "albums_on_device_count": "Albumi ierīcē ({count})", - "all": "Viss", + "all": "Visi", "all_albums": "Visi albumi", - "all_people": "Visi cilvēki", + "all_people": "Visas personas", "all_videos": "Visi video", "allow_dark_mode": "Atļaut tumšo režīmu", "allow_edits": "Atļaut labošanu", @@ -279,6 +290,7 @@ "app_bar_signout_dialog_title": "Izrakstīties", "app_settings": "Lietotnes iestatījumi", "appears_in": "Parādās iekš", + "apply_count": "Pielietot ({count, number})", "archive": "Arhīvs", "archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs", "archive_page_title": "Arhīvs ({count})", @@ -293,7 +305,7 @@ "asset_list_group_by_sub_title": "Grupēt pēc", "asset_list_layout_settings_dynamic_layout_title": "Dinamiskais izkārtojums", "asset_list_layout_settings_group_automatically": "Automātiski", - "asset_list_layout_settings_group_by": "Grupēt aktīvus pēc", + "asset_list_layout_settings_group_by": "Grupēt failus pēc", "asset_list_layout_settings_group_by_month_day": "Mēnesis + diena", "asset_list_layout_sub_title": "Izvietojums", "asset_list_settings_subtitle": "Fotorežģa izkārtojuma iestatījumi", @@ -302,8 +314,8 @@ "asset_skipped_in_trash": "Atkritnē", "asset_uploaded": "Augšupielādēts", "asset_uploading": "Augšupielādē…", - "asset_viewer_settings_title": "Aktīvu Skatītājs", - "assets": "aktīvi", + "asset_viewer_settings_title": "Failu skatītājs", + "assets": "Faili", "assets_added_count": "Pievienoja {count, plural, one {# failu} other {# failus}}", "assets_added_to_album_count": "Pievienoja albumam {count, plural, one {# failu} other {# failus}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Failu} other {Failus}} nevar pievienot albumam", @@ -314,14 +326,17 @@ "assets_trashed_from_server": "{count} faili pārvietoti uz Immich servera atkritni", "authorized_devices": "Autorizētās ierīces", "automatic_endpoint_switching_title": "Automātiska URL pārslēgšana", + "autoplay_slideshow": "Automātiska slaidrādes atskaņošana", "back": "Atpakaļ", + "background_options": "Fona opcijas", "backup": "Dublēšana", "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.", + "backup_album_selection_page_assets_scatter": "Faili var būt izmētāti pa vairākiem albumiem. Tādējādi dublēšanas procesā albumus var iekļaut vai neiekļaut.", "backup_album_selection_page_select_albums": "Atlasīt albumus", "backup_album_selection_page_selection_info": "Atlases informācija", - "backup_album_selection_page_total_assets": "Kopā unikālie aktīvi", + "backup_album_selection_page_total_assets": "Unikālo failu kopsumma", + "backup_albums_sync": "Dublēšanas albumu sinhronizācija", "backup_all": "Viss", "backup_background_service_backup_failed_message": "Neizdevās dublēt līdzekļus. Notiek atkārtota mēģināšana…", "backup_background_service_connection_failed_message": "Neizdevās izveidot savienojumu ar serveri. Notiek atkārtota mēģināšana…", @@ -336,6 +351,7 @@ "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": "Labi", "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", @@ -370,7 +386,7 @@ "backup_controller_page_turn_on": "Ieslēgt priekšplāna dublēšanu", "backup_controller_page_uploading_file_info": "Faila informācijas augšupielāde", "backup_err_only_album": "Nevar noņemt vienīgo albumu", - "backup_info_card_assets": "aktīvi", + "backup_info_card_assets": "faili", "backup_manual_cancelled": "Atcelts", "backup_manual_in_progress": "Augšupielāde jau notiek. Mēģiniet pēc kāda laika atkārtoti", "backup_manual_success": "Veiksmīgi", @@ -378,8 +394,6 @@ "backup_options_page_title": "Dublēšanas iestatījumi", "backup_settings_subtitle": "Pārvaldīt augšupielādes iestatījumus", "backward": "Atpakaļejoši", - "beta_sync": "Beta Sinhronizācijas statuss", - "beta_sync_subtitle": "Pārvaldīt jauno sinhronizācijas sistēmu", "biometric_auth_enabled": "Ieslēgta biometriskā autentifikācija", "biometric_locked_out": "Biometriskā autentifikācija tev ir bloķēta", "biometric_no_options": "Nav pieejamas biometriskās autentifikācijas iespējas", @@ -405,8 +419,11 @@ "cache_settings_title": "Kešdarbes iestatījumi", "camera": "Fotokamera", "cancel": "Atcelt", + "canceled": "Atcelts", "canceling": "Atceļ", - "cannot_merge_people": "Nevar apvienot cilvēkus", + "cannot_merge_people": "Nevar apvienot personas", + "cast": "Pārraidīt", + "cast_description": "Konfigurēt pieejamos pārraides galamērķus", "change_date": "Mainīt datumu", "change_description": "Mainīt aprakstu", "change_display_order": "Mainīt attēlošanas secību", @@ -421,12 +438,21 @@ "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", - "choose_matching_people_to_merge": "Izvēlies atbilstošus cilvēkus apvienošanai", + "charging": "Lādē", + "charging_requirement_mobile_backup": "Fona dublēšanai nepieciešams, lai ierīce tiktu lādēta", + "check_corrupt_asset_backup_button": "Veikt pārbaudi", + "choose_matching_people_to_merge": "Izvēlies atbilstošas personas apvienošanai", "city": "Pilsēta", "clear": "Notīrīt", "clear_all": "Notīrīt visu", "clear_file_cache": "Notīrīt failu kešatmiņu", "clear_value": "Notīrīt vērtību", + "client_cert_dialog_msg_confirm": "Labi", + "client_cert_enter_password": "Ievadi paroli", + "client_cert_import": "Importēt", + "client_cert_import_success_msg": "Klienta sertifikāts ir importēts", + "client_cert_invalid_msg": "Nederīgs sertifikāta fails vai nepareiza parole", + "client_cert_remove_msg": "Klienta sertifikāts ir noņemts", "client_cert_subtitle": "Atbalsta tikai PKCS12 (.p12, .pfx) formātu. Sertifikātu importēšana/noņemšana ir pieejama tikai pirms pieslēgšanās", "client_cert_title": "SSL klienta sertifikāts", "clockwise": "Pulksteņrādītāja virzienā", @@ -438,6 +464,7 @@ "comment_deleted": "Komentārs dzēsts", "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": "Pabeigts", "confirm": "Apstiprināt", "confirm_new_pin_code": "Apstiprināt jauno PIN kodu", "confirm_password": "Apstiprināt paroli", @@ -448,11 +475,12 @@ "control_bottom_app_bar_create_new_album": "Izveidot jaunu albumu", "control_bottom_app_bar_delete_from_immich": "Dzēst no Immich", "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_to": "Kopīgot Uz", + "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_to": "Kopīgot uz", "control_bottom_app_bar_trash_from_immich": "Pārvietot uz Atkritni", "copy_error": "Kopēšanas kļūda", + "copy_to_clipboard": "Kopēt starpliktuvē", "country": "Valsts", "create": "Izveidot", "create_album": "Izveidot albumu", @@ -460,6 +488,7 @@ "create_library": "Izveidot bibliotēku", "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopīgošanas saiti", + "create_new": "IZVEIDOT JAUNU", "create_new_person": "Izveidot jaunu personu", "create_new_user": "Izveidot jaunu lietotāju", "create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS", @@ -468,8 +497,12 @@ "created_at": "Izveidots", "curated_object_page_title": "Lietas", "current_pin_code": "Esošais PIN kods", + "current_server_address": "Pašreizējā servera adrese", + "custom_locale": "Pielāgota lokalizācija", + "custom_locale_description": "Formatēt datumus un skaitļus atbilstoši valodai un reģionam", "custom_url": "Pielāgots URL", "daily_title_text_date_year": "E, MMM dd, gggg", + "dark_theme": "Pārslēgt tumšo tēmu", "date_after": "Datums pēc", "date_and_time": "Datums un Laiks", "date_before": "Datums pirms", @@ -483,6 +516,8 @@ "deduplication_criteria_2": "EXIF datu skaitu", "deduplication_info": "Deduplicēšanas informācija", "deduplication_info_description": "Lai automātiski atzīmētu failus un masveidā noņemtu dublikātus, mēs skatāmies uz:", + "default_locale": "Noklusējuma lokalizācija", + "default_locale_description": "Formatēt datumus un skaitļus atbilstoši pārlūka lokalizācijai", "delete": "Dzēst", "delete_album": "Dzēst albumu", "delete_dialog_alert": "Šie vienumi tiks neatgriezeniski dzēsti no Immich un jūsu ierīces", @@ -510,27 +545,34 @@ "direction": "Secība", "discord": "Discord", "display_order": "Attēlošanas secība", + "display_original_photos": "Rādīt oriģinālās fotogrāfijas", "documentation": "Dokumentācija", "done": "Gatavs", "download": "Lejupielādēt", "download_action_prompt": "Lejupielādē {count} failus", "download_canceled": "Lejupielāde atcelta", "download_complete": "Lejupielāde pabeigta", + "download_enqueue": "Lejupielāde ierindota", "download_error": "Lejupielādes kļūda", "download_failed": "Lejupielāde neizdevās", + "download_finished": "Lejupielāde pabeigta", "download_notfound": "Lejupielāde nav atrasta", "download_paused": "Lejupielāde nopauzēta", "download_settings": "Lejupielāde", "download_settings_description": "Ar failu lejupielādi saistīto iestatījumu pārvaldība", "download_started": "Lejupielāde sākta", "download_sucess": "Lejupielāde izdevās", + "download_sucess_android": "Multivides fails ir lejupielādēts uz DCIM/Immich", + "download_waiting_to_retry": "Gaida, lai mēģinātu atkārtoti", "downloading": "Lejupielādē", "downloading_asset_filename": "Lejupielādē failu {filename}", + "downloading_media": "Lejupielādē failu", "duplicates": "Dublikāti", "duplicates_description": "Atrisini katru grupu, norādot, kuri no tiem ir dublikāti", "duration": "Ilgums", "edit": "Labot", "edit_album": "Labot albumu", + "edit_avatar": "Labot avatāru", "edit_birthday": "Labot dzimšanas dienu", "edit_date": "Labot datumu", "edit_date_and_time": "Labot datumu un laiku", @@ -556,6 +598,7 @@ "email_notifications": "E-pasta paziņojumi", "empty_folder": "Šī mape ir tukša", "empty_trash": "Iztukšot atkritni", + "enable_backup": "Ieslēgt dublēšanu", "enable_biometric_auth_description": "Lai iespējotu biometrisko autentifikāciju, Ievadiet savu PIN kodu", "end_date": "Beigu datums", "enqueued": "Ierindots", @@ -563,11 +606,27 @@ "enter_your_pin_code": "Ievadi savu PIN kodu", "enter_your_pin_code_subtitle": "Ievadi savu PIN kodu, lai piekļūtu slēgtajai mapei", "error": "Kļūda", + "error_change_sort_album": "Neizdevās nomainīt albuma kārtošanas secību", + "error_loading_partners": "Kļūda, ielādējot partnerus: {error}", "error_saving_image": "Kļūda: {error}", "errors": { "cant_get_faces": "Nevar iegūt sejas", "cant_search_people": "Neizdevās veikt peronu meklēšanu", + "exclusion_pattern_already_exists": "Šāds izslēgšanas paraugs jau pastāv.", "failed_to_create_album": "Neizdevās izveidot albumu", + "failed_to_create_shared_link": "Neizdevās izvedot kopīgošanas saiti", + "failed_to_edit_shared_link": "Neizdevās labot kopīgoto saiti", + "failed_to_get_people": "Neizdevās iegūt personas", + "failed_to_keep_this_delete_others": "Neizdevās paturēt šo failu un dzēst pārējos failus", + "failed_to_load_asset": "Neizdevās ielādēt failu", + "failed_to_load_assets": "Neizdevās ielādēt failus", + "failed_to_load_notifications": "Neizdevās ielādēt paziņojumus", + "failed_to_load_people": "Neizdevās ielādēt personas", + "failed_to_remove_product_key": "Neizdevās noņemt produkta atslēgu", + "failed_to_reset_pin_code": "Neizdevās atiestatīt PIN kodu", + "failed_to_stack_assets": "Neizdevās apvienot failus kaudzē", + "failed_to_unstack_assets": "Neizdevās atcelt failu apvienošanu kaudzē", + "failed_to_update_notification_status": "Neizdevās mainīt paziņojuma statusu", "import_path_already_exists": "Šis importa ceļš jau pastāv.", "incorrect_email_or_password": "Nepareizs e-pasts vai parole", "profile_picture_transparent_pixels": "Profila attēlos nevar būt caurspīdīgi pikseļi. Lūdzu, palielini un/vai pārvieto attēlu.", @@ -587,7 +646,7 @@ "exif_bottom_sheet_description": "Pievienot Aprakstu...", "exif_bottom_sheet_details": "INFORMĀCIJA", "exif_bottom_sheet_location": "ATRAŠANĀS VIETA", - "exif_bottom_sheet_people": "CILVĒKI", + "exif_bottom_sheet_people": "PERSONAS", "exif_bottom_sheet_person_add_person": "Pievienot vārdu", "exit_slideshow": "Iziet no slīdrādes", "experimental_settings_new_asset_list_subtitle": "Izstrādes posmā", @@ -598,24 +657,30 @@ "expired": "Derīguma termiņš beidzās", "explore": "Izpētīt", "export": "Eksportēt", + "export_as_json": "Eksportēt kā JSON", "export_database": "Eksportēt datubāzi", "export_database_description": "Eksportēt SQLite datubāzi", "extension": "Paplašinājums", "external": "Ārējs", - "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", + "external_libraries": "Ārējas bibliotēkas", + "external_network": "Ārējs tīkls", + "external_network_sheet_info": "Kad nav pieejams izvēlētais Wi-Fi tīkls, aplikācija pieslēgsies serverim lietojot pirmo strādājošo URL no saraksta, sākot ar augšējo", "face_unassigned": "Nepiešķirts", + "failed": "Neizdevās", "failed_to_authenticate": "Neizdevās autentificēties", "failed_to_load_assets": "Neizdevās ielādēt failus", + "failed_to_load_folder": "Neizdevās ielādēt mapi", "favorite": "Izlase", "favorites": "Izlase", "favorites_page_no_favorites": "Nav atrasti iecienītākie faili", + "features_in_development": "Izstrādes stadijā esošas funkcijas", "features_setting_description": "Lietotnes funkciju pārvaldība", "file_name": "Faila nosaukums", "file_name_or_extension": "Faila nosaukums vai paplašinājums", "filename": "Faila nosaukums", "filetype": "Faila tips", "filter": "Filtrēt", - "filter_people": "Filtrēt cilvēkus", + "filter_people": "Filtrēt personas", "filter_places": "Filtrēt vietas", "first": "Pirmais", "folder": "Mape", @@ -624,12 +689,15 @@ "forgot_pin_code_question": "Aizmirsi savu PIN?", "forward": "Uz priekšu", "gcast_enabled": "Google Cast", + "gcast_enabled_description": "Šī funkcija darbojas, lejupielādējot ārējos resursus no Google.", "get_help": "Saņemt palīdzību", "get_wifiname_error": "Nevarēja iegūt Wi-Fi nosaukumu. Pārliecinies, ka esi piešķīris nepieciešamās atļaujas un esi savienots ar Wi-Fi tīklu", "getting_started": "Pirmie soļi", "go_back": "Doties atpakaļ", "go_to_folder": "Doties uz mapi", "go_to_search": "Doties uz meklēšanu", + "gps": "Ir koordinātas", + "gps_missing": "Nav koordinātu", "grant_permission": "Piešķirt atļauju", "group_albums_by": "Grupēt albumus pēc...", "group_country": "Grupēt pēc valsts", @@ -637,20 +705,20 @@ "group_owner": "Grupēt pēc īpašnieka", "group_places_by": "Grupēt vietas pēc...", "group_year": "Grupēt pēc gada", - "haptic_feedback_switch": "Iestatīt haptisku reakciju", + "haptic_feedback_switch": "Iespējot haptisku reakciju", "haptic_feedback_title": "Haptiska Reakcija", - "has_quota": "Ir kvota", + "has_quota": "Kvota", "hash_asset": "Veidot faila jaucējvērtību", - "hashed_assets": "Faili ar izveidotām jaucējvērtībām", + "hashed_assets": "Faili ar jaucējvērtībām", "hashing": "Veido jaucējvērtības", "header_settings_field_validator_msg": "Vērtība nevar būt tukša", - "hide_all_people": "Paslēpt visus cilvēkus", + "hide_all_people": "Paslēpt visas personas", "hide_gallery": "Paslēpt galeriju", "hide_named_person": "Paslēpt personu {name}", "hide_password": "Paslēpt paroli", "hide_person": "Paslēpt personu", "hide_unnamed_people": "Paslēpt nenosauktas personas", - "home_page_add_to_album_conflicts": "Pievienoja {added} aktīvus albumam {album}. {failed} aktīvi jau ir albumā.", + "home_page_add_to_album_conflicts": "Pievienoja {added} failus albumam {album}. {failed} faili jau ir albumā.", "home_page_add_to_album_err_local": "Albumiem vēl nevar pievienot lokālos aktīvus, notiek izlaišana", "home_page_add_to_album_success": "Pievienoja {added} aktīvus albumam {album}.", "home_page_album_err_partner": "Pagaidām nevar pievienot partnera aktīvus albumam, notiek izlaišana", @@ -658,7 +726,7 @@ "home_page_archive_err_partner": "Nevarēja arhivēt partnera aktīvus, notiek izlaišana", "home_page_building_timeline": "Tiek izveidota laika skala", "home_page_delete_err_partner": "Nevarēja dzēst partnera aktīvus, notiek izlaišana", - "home_page_delete_remote_err_local": "Lokālie aktīvi dzēšanai attālinātajā izvēlē, tiek izlaists", + "home_page_delete_remote_err_local": "Lokālie faili dzēšanai attālinātajā izvēlē, tiek izlaists", "home_page_favorite_err_local": "Vēl nevar pievienot izlasei vietējos failus, izlaiž", "home_page_favorite_err_partner": "Pagaidām nevar ievietot izlasē partnera failus, izlaiž", "home_page_first_time_notice": "Ja šī ir pirmā reize, kad izmanto lietotni, lūdzu, izvēlies dublējamo albumu, lai laika skalā varētu aizpildīt fotoattēlus un videoklipus", @@ -685,6 +753,7 @@ "in_archive": "Arhīvā", "include_archived": "Iekļaut arhivētos", "include_shared_albums": "Iekļaut koplietotos albumus", + "include_shared_partner_assets": "Iekļaut partneru koplietotos failus", "info": "Informācija", "interval": { "day_at_onepm": "Katru dienu 13.00", @@ -695,16 +764,21 @@ "invalid_date_format": "Nederīgs datuma formāts", "invite_people": "Ielūgt cilvēkus", "invite_to_album": "Uzaicināt albumā", + "ios_debug_info_fetch_ran_at": "Ielasīšana notika {dateTime}", "ios_debug_info_last_sync_at": "Pēdējā sinhronizācija {dateTime}", "ios_debug_info_no_processes_queued": "Nav ierindotu fona procesu", + "ios_debug_info_processing_ran_at": "Apstrāde notika {dateTime}", + "items_count": "{count, plural, one {# vienums} other {# vienumi}}", "jobs": "Uzdevumi", "keep": "Paturēt", "keep_all": "Paturēt visus", "keep_this_delete_others": "Paturēt šo, dzēst citus", "keyboard_shortcuts": "Tastatūras saīsnes", "language": "Valoda", + "language_no_results_subtitle": "Mēģini pielāgot meklēšanas terminu", + "language_no_results_title": "Nav atrasta neviena valoda", "language_search_hint": "Meklēt valodas...", - "language_setting_description": "Izvēlieties vēlamo valodu", + "language_setting_description": "Izvēlies vēlamo valodu", "large_files": "Lielie faili", "last": "Pēdējais", "last_seen": "Pēdējo reizi redzēts", @@ -716,6 +790,7 @@ "let_others_respond": "Ļaut citiem atbildēt", "level": "Līmenis", "library": "Bibliotēka", + "library_options": "Bibliotēkas opcijas", "library_page_device_albums": "Albumi ierīcē", "library_page_new_album": "Jauns albums", "library_page_sort_asset_count": "Failu skaits", @@ -723,9 +798,14 @@ "library_page_sort_last_modified": "Pēdējās izmaiņas", "library_page_sort_title": "Albuma virsraksts", "licenses": "Licences", + "link_to_oauth": "Piesaistīt OAuth", + "linked_oauth_account": "Piesaistītais OAuth konts", "list": "Saraksts", "loading": "Ielādē", - "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", + "local": "Lokāli", + "local_network": "Lokālais tīkls", + "location_permission": "Atrašanās vietas atļauja", + "location_permission_content": "Lai izmantotu automātiskās pārslēgšanās funkciju, Immich ir nepieciešama precīzas atrašanās vietas atļauja, lai varētu nolasīt pašreizējā Wi-Fi tīkla nosaukumu", "location_picker_choose_on_map": "Izvēlēties uz kartes", "location_picker_latitude_error": "Ievadiet korektu ģeogrāfisko platumu", "location_picker_latitude_hint": "Ievadiet savu ģeogrāfisko platumu šeit", @@ -759,6 +839,7 @@ "look": "Izskats", "loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaļu skatītājā.", "make": "Ražotājs", + "manage_geolocation": "Pārvaldīt atrašanās vietu", "manage_shared_links": "Kopīgoto saišu pārvaldība", "manage_sharing_with_partners": "Koplietošanas ar partneriem pārvaldība", "manage_the_app_settings": "Lietotnes iestatījumu pārvaldība", @@ -771,17 +852,17 @@ "map_cannot_get_user_location": "Nevar iegūt lietotāja atrašanās vietu", "map_location_dialog_yes": "Jā", "map_location_picker_page_use_location": "Izvēlēties šo atrašanās vietu", - "map_location_service_disabled_content": "Lai tiktu rādīti jūsu pašreizējās atrašanās vietas aktīvi, ir jāaktivizē atrašanās vietas pakalpojums. Vai vēlaties to iespējot tagad?", + "map_location_service_disabled_content": "Lai tiktu rādīti jūsu pašreizējās atrašanās vietas faili, ir jāaktivizē atrašanās vietas pakalpojums. Vai vēlaties to iespējot tagad?", "map_location_service_disabled_title": "Atrašanās vietas Pakalpojums atslēgts", "map_marker_for_images": "Kartes marķieris attēliem, kas uzņemti {city}, {country}", "map_marker_with_image": "Kartes marķieris ar attēlu", "map_no_location_permission_content": "Atrašanās vietas atļauja ir nepieciešama, lai parādītu jūsu pašreizējās atrašanās vietas aktīvus. Vai vēlaties to atļaut tagad?", "map_no_location_permission_title": "Atrašanās vietas Atļaujas liegtas", - "map_settings": "Kartes Iestatījumi", + "map_settings": "Kartes iestatījumi", "map_settings_dark_mode": "Tumšais režīms", "map_settings_date_range_option_day": "Pēdējās 24 stundas", "map_settings_date_range_option_days": "Pēdējās {days} dienas", - "map_settings_date_range_option_year": "Pēdējo gadu", + "map_settings_date_range_option_year": "Pēdējais gads", "map_settings_date_range_option_years": "Pēdējie {years} gadi", "map_settings_dialog_title": "Kartes Iestatījumi", "map_settings_include_show_archived": "Iekļaut Arhivētos", @@ -790,7 +871,7 @@ "map_settings_theme_settings": "Kartes Dizains", "map_zoom_to_see_photos": "Attāliniet, lai redzētu fotoattēlus", "matches": "Atbilstības", - "media_type": "Multivides veids", + "media_type": "Faila veids", "memories": "Atmiņas", "memories_all_caught_up": "Šobrīd, tas arī viss", "memories_check_back_tomorrow": "Atgriezies rīt, lai skatītu vairāk atmiņu", @@ -799,10 +880,10 @@ "memory": "Atmiņa", "menu": "Izvēlne", "merge": "Apvienot", - "merge_people": "Cilvēku apvienošana", + "merge_people": "Personu apvienošana", "merge_people_limit": "Vienlaikus var apvienot ne vairāk kā 5 sejas", - "merge_people_prompt": "Vai vēlies apvienot šos cilvēkus? Šī darbība ir neatgriezeniska.", - "merge_people_successfully": "Cilvēki veiksmīgi apvienoti", + "merge_people_prompt": "Vai vēlies apvienot šīs personas? Šī darbība ir neatceļama.", + "merge_people_successfully": "Personas veiksmīgi apvienotas", "minimize": "Minimizēt", "minute": "Minūte", "minutes": "Minūtes", @@ -826,12 +907,16 @@ "name_or_nickname": "Vārds vai iesauka", "network_requirement_photos_upload": "Izmantot mobilo datu pārraidi, lai dublētu fotoattēlus", "network_requirement_videos_upload": "Izmantot mobilo datu pārraidi, lai dublētu video", + "network_requirements": "Tīkla prasības", + "networking_settings": "Tīkla iestatījumi", + "networking_subtitle": "Pārvaldīt servera galapunktu iestatījumus", "never": "nekad", "new_album": "Jauns albums", "new_api_key": "Jauna API atslēga", "new_password": "Jaunā parole", "new_person": "Jauna persona", "new_pin_code": "Jaunais PIN kods", + "new_timeline": "Jaunā laikjosla", "new_user_created": "Izveidots jauns lietotājs", "new_version_available": "PIEEJAMA JAUNA VERSIJA", "next": "Nākamais", @@ -851,12 +936,13 @@ "no_results": "Nav rezultātu", "no_results_description": "Izmēģiniet sinonīmu vai vispārīgāku atslēgvārdu", "not_in_any_album": "Nav nevienā albumā", + "not_selected": "Nav izvēlēts", "notes": "Piezīmes", "nothing_here_yet": "Šeit vēl nekā nav", "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.", - "notification_permission_list_tile_enable_button": "Iespējot Paziņojumus", - "notification_permission_list_tile_title": "Paziņojumu Atļaujas", + "notification_permission_list_tile_enable_button": "Iespējot paziņojumus", + "notification_permission_list_tile_title": "Paziņojumu atļaujas", "notification_toggle_setting_description": "Ieslēgt e-pasta paziņojumus", "notifications": "Paziņojumi", "notifications_setting_description": "Paziņojumu pārvaldība", @@ -867,6 +953,7 @@ "ok": "Labi", "onboarding": "Uzņemšana", "onboarding_locale_description": "Izvēlies vēlamo valodu. To vēlāk var mainīt iestatījumos.", + "onboarding_server_welcome_description": "Iestatīsim šo instanci ar dažiem vispārīgiem iestatījumiem.", "onboarding_theme_description": "Izvēlies savas instances krāsu motīvu. To vēlāk var mainīt iestatījumos.", "onboarding_user_welcome_description": "Sāksim darbu!", "online": "Tiešsaistē", @@ -876,6 +963,8 @@ "open_the_search_filters": "Atvērt meklēšanas filtrus", "options": "Iestatījumi", "or": "vai", + "organize_into_albums": "Sakārtot albumos", + "organize_into_albums_description": "Ievietot esošās fotogrāfijas albumos, izmantojot pašreizējos sinhronizācijas iestatījumus", "organize_your_library": "Bibliotēkas organizēšana", "original": "oriģināls", "other": "Citi", @@ -894,6 +983,7 @@ "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": "Koplietošana ar partneriem", "partners": "Partneri", "password": "Parole", "password_does_not_match": "Parole nesakrīt", @@ -901,7 +991,10 @@ "pause": "Pauzēt", "pause_memories": "Pauzēt atmiņas", "paused": "Nopauzēts", - "people": "Cilvēki", + "people": "Personas", + "people_sidebar_description": "Parādīt saiti uz personām sānu joslā", + "permission": "Atļauja", + "permission_empty": "Tava atļauja nedrīkst būt tukša", "permission_onboarding_back": "Atpakaļ", "permission_onboarding_continue_anyway": "Tomēr turpināt", "permission_onboarding_get_started": "Darba sākšana", @@ -926,6 +1019,9 @@ "preview": "Priekšskatījums", "previous": "Iepriekšējais", "previous_memory": "Iepriekšējā atmiņa", + "previous_or_next_day": "Dienu uz priekšu/atpakaļ", + "previous_or_next_month": "Mēnesi uz priekšu/atpakaļ", + "previous_or_next_year": "Gadu uz priekšu/atpakaļ", "privacy": "Privātums", "profile": "Profils", "profile_drawer_app_logs": "Žurnāli", @@ -975,6 +1071,7 @@ "rating_description": "Rādīt EXIF vērtējumu informācijas panelī", "reaction_options": "Reakcijas iespējas", "read_changelog": "Lasīt izmaiņu sarakstu", + "ready_for_upload": "Gatavs augšupielādei", "recently_added_page_title": "Nesen Pievienotais", "refresh": "Atsvaidzināt", "refresh_faces": "Atsvaidzināt sejas", @@ -984,8 +1081,10 @@ "refreshes_every_file": "Vēlreiz nolasa esošos un jaunos failus", "refreshing_faces": "Atsvaidzina sejas", "refreshing_metadata": "Atsvaidzina metadatus", + "remote": "Attāli", "remove": "Noņemt", "remove_assets_title": "Izņemt failus?", + "remove_custom_date_range": "Novākt pielāgoto datuma intervālu", "remove_deleted_assets": "Izņemt dzēstos failus", "remove_from_album": "Noņemt no albuma", "remove_from_album_action_prompt": "No albuma izņemti {count} faili", @@ -1000,6 +1099,8 @@ "removed_from_archive": "Noņēma no arhīva", "removed_from_favorites": "Noņēma no izlases", "removed_from_favorites_count": "{count, plural, other {Izņēma #}} no izlases", + "removed_memory": "Noņēma atmiņu", + "removed_photo_from_memory": "Noņēma fotogrāfiju no atmiņas", "rename": "Pārsaukt", "repair": "Remonts", "replace_with_upload": "Aizstāt ar augšupielādi", @@ -1008,8 +1109,9 @@ "rescan": "Pārskenēt atkārtoti", "reset": "Atiestatīt", "reset_password": "Atiestatīt paroli", - "reset_people_visibility": "Atiestatīt cilvēku redzamību", + "reset_people_visibility": "Atiestatīt personu redzamību", "reset_pin_code": "Atiestatīt PIN kodu", + "reset_sqlite": "Atiestatīt SQLite datubāzi", "reset_to_default": "Atiestatīt noklusējuma iestatījumus", "resolve_duplicates": "Atrisināt dublēšanās gadījumus", "resolved_all_duplicates": "Visi dublikāti ir atrisināti", @@ -1056,8 +1158,9 @@ "search_filter_media_type": "Multivides veids", "search_filter_media_type_title": "Izvēlies multivides veidu", "search_for_existing_person": "Meklēt esošu personu", - "search_no_people": "Nav cilvēku", - "search_no_people_named": "Nav cilvēku ar vārdu \"{name}\"", + "search_no_people": "Nav personu", + "search_no_people_named": "Nav personas ar vārdu \"{name}\"", + "search_options": "Meklēšanas iespējas", "search_page_categories": "Kategorijas", "search_page_motion_photos": "Kustību Fotoattēli", "search_page_no_objects": "Informācija par Objektiem nav pieejama", @@ -1068,25 +1171,33 @@ "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_people": "Meklēt personas", "search_result_page_new_search_hint": "Jauns Meklējums", + "search_settings": "Meklēt iestatījumos", + "search_state": "Meklēt pēc štata...", "search_suggestion_list_smart_search_hint_1": "Viedā meklēšana pēc noklusējuma ir iespējota, lai meklētu metadatos, izmanto sintaksi ", "search_suggestion_list_smart_search_hint_2": "m:jūsu-meklēšanas-frāze", "search_type": "Meklēšanas veids", "search_your_photos": "Meklēt Jūsu fotoattēlus", "second": "Sekunde", - "see_all_people": "Skatīt visus cilvēkus", + "see_all_people": "Skatīt visas personas", "select_album_cover": "Izvēlieties albuma vāciņu", "select_all_duplicates": "Atlasīt visus dublikātus", + "select_avatar_color": "Izvēlies avatāra krāsu", + "select_face": "Izvēlies seju", "select_from_computer": "Izvēlēties no datora", "select_keep_all": "Atzīmēt visus paturēšanai", + "select_library_owner": "Izvēlies bibliotēkas īpašnieku", + "select_new_face": "Izvēlies jaunu seju", "select_photos": "Fotoattēlu Izvēle", - "select_trash_all": "Atzīmēt visus pārvietošanai uz atkritni", + "select_trash_all": "Atzīmēt visus dzēšanai", "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", "selected": "Izvēlētie", + "selected_gps_coordinates": "Izvēlētās ģeogrāfiskās koordinātas", "server_info_box_app_version": "Aplikācijas Versija", "server_info_box_server_url": "Servera URL", "server_online": "Serveris tiešsaistē", + "server_privacy": "Servera privātums", "server_stats": "Servera statistika", "server_version": "Servera versija", "set_date_of_birth": "Iestatīt dzimšanas datumu", @@ -1103,12 +1214,14 @@ "setting_notifications_notify_minutes": "{count} minūtes", "setting_notifications_notify_never": "nekad", "setting_notifications_notify_seconds": "{count} sekundes", - "setting_notifications_single_progress_subtitle": "Detalizēta augšupielādes progresa informācija par katru aktīvu", + "setting_notifications_single_progress_subtitle": "Detalizēta augšupielādes progresa informācija par katru failu", "setting_notifications_single_progress_title": "Rādīt fona dublējuma detalizēto progresu", "setting_notifications_subtitle": "Paziņojumu preferenču pielāgošana", - "setting_notifications_total_progress_subtitle": "Kopējais augšupielādes progress (pabeigti/kopējie aktīvi)", + "setting_notifications_total_progress_subtitle": "Kopējais augšupielādes progress (pabeigti/kopējie faili)", "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": "Straumējot video no servera, izmantot oriģinālu, pat ja ir pieejama pārkodēšana. Tas var izraisīt buferēšanu. Lokāli pieejamie video tiek atskaņoti oriģinālajā kvalitātē, neatkarīgi no šīs iestatījuma.", + "setting_video_viewer_original_video_title": "Vienmēr izmantot oriģinālo video", "settings": "Iestatījumi", "settings_require_restart": "Lūdzu, restartējiet Immich, lai lietotu šo iestatījumu", "setup_pin_code": "Uzstādīt PIN kodu", @@ -1123,7 +1236,7 @@ "shared_album_section_people_action_error": "Kļūme pametot/noņemot no albuma", "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_album_section_people_title": "PERSONAS", "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ē", @@ -1159,32 +1272,47 @@ "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": "Parādīt saiti uz kopīgošanu sānu joslā", "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", "show_albums": "Rādīt albumus", - "show_all_people": "Rādīt visus cilvēkus", - "show_and_hide_people": "Rādīt un slēpt cilvēkus", + "show_all_people": "Rādīt visas personas", + "show_and_hide_people": "Rādīt un slēpt personas", "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_hidden_people": "Rādīt paslēptās personas", "show_in_timeline": "Parādīt laika skalā", "show_in_timeline_setting_description": "Rādīt šī lietotāja fotogrāfijas un video tavā laika skalā", + "show_keyboard_shortcuts": "Rādīt tastatūras saīsnes", "show_metadata": "Rādīt metadatus", + "show_or_hide_info": "Rādīt vai slēpt informāciju", + "show_password": "Parādīt paroli", + "show_person_options": "Rādīt personas opcijas", "show_progress_bar": "Rādīt progresa joslu", + "show_search_options": "Rādīt meklēšanas opcijas", + "show_shared_links": "Rādīt kopīgotās saites", + "show_slideshow_transition": "Rādīt slīdrādes pāreju", "show_supporter_badge": "Atbalstītāja nozīmīte", "show_supporter_badge_description": "Rādīt atbalstītāja nozīmīti", + "show_text_search_menu": "Rādīt teksta meklēšanas izvēlni", "shuffle": "Jaukta", + "sidebar": "Sānu josla", + "sidebar_display_description": "Parādīt saiti uz skatu sānu joslā", + "sign_out": "Iziet", + "sign_up": "Reģistrēties", "size": "Izmērs", + "skip_to_content": "Pāriet uz saturu", + "skip_to_folders": "Pāriet uz mapēm", "slideshow": "Slīdrāde", "slideshow_settings": "Slīdrādes iestatījumi", "sort_albums_by": "Kārtot albumus pēc...", "sort_created": "Izveides datums", - "sort_items": "Vienību skaits", + "sort_items": "Vienumu skaits", "sort_modified": "Izmaiņu datums", "sort_newest": "Jaunākā fotogrāfija", "sort_oldest": "Vecākā fotogrāfija", - "sort_people_by_similarity": "Sakārtot cilvēkus pēc līdzības", + "sort_people_by_similarity": "Sakārtot personas pēc līdzības", "sort_recent": "Nesenākā fotogrāfija", "sort_title": "Nosaukums", "source": "Pirmkods", @@ -1203,11 +1331,18 @@ "support": "Atbalsts", "support_and_feedback": "Atbalsts un atsauksmes", "sync": "Sinhronizēt", + "sync_status": "Sinhronizācijas statuss", + "sync_status_subtitle": "Skatīt un pārvaldīt sinhronizācijas sistēmu", "theme": "Dizains", - "theme_setting_asset_list_storage_indicator_title": "Rādīt krātuves indikatoru uz aktīvu elementiem", + "theme_setting_asset_list_storage_indicator_title": "Rādīt krātuves indikatoru uz attēliem režga skatā", "theme_setting_asset_list_tiles_per_row_title": "Failu skaits rindā ({count})", + "theme_setting_colorful_interface_subtitle": "Piemērot pamatkrāsu fona virsmām.", + "theme_setting_colorful_interface_title": "Krāsaina saskarne", "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": "Izvēlies krāsu galvenajām darbībām un akcentiem.", + "theme_setting_primary_color_title": "Pamatkrāsa", + "theme_setting_system_primary_color_title": "Izmantot sistēmas krāsu", "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", @@ -1225,19 +1360,20 @@ "total_usage": "Kopējais lietojums", "trash": "Atkritne", "trash_action_prompt": "{count} pārvietoja uz atkritni", - "trash_all": "Dzēst Visu", + "trash_all": "Dzēst visu", "trash_count": "Pārvietot uz atkritni {count, number}", "trash_delete_asset": "Pārvietot uz atkritni/dzēst failu", "trash_emptied": "Atkritne iztukšota", "trash_no_results_message": "Šeit parādīsies uz atkritni pārvietotās fotogrāfijas un video.", "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_empty_trash_dialog_content": "Vai vēlaties iztukšot savus izmestos failus? Tie tiks neatgriezeniski izņemti no Immich", "trash_page_info": "Atkritnes vienumi tiks neatgriezeniski dzēsti pēc {days} dienām", "trash_page_no_assets": "Atkritnē nav aktīvu", "trash_page_restore_all": "Atjaunot Visu", "trash_page_select_assets_btn": "Atlasīt aktīvus", "trash_page_title": "Atkritne ({count})", "trashed_items_will_be_permanently_deleted_after": "Faili no atkritnes tiks neatgriezeniski dzēsti pēc {days, plural, one {# dienas} other {# dienām}}.", + "troubleshoot": "Problēmu novēršana", "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", @@ -1262,21 +1398,27 @@ "upload_status_errors": "Kļūdas", "upload_status_uploaded": "Augšupielādēts", "upload_to_immich": "Augšupielādēt Immich ({count})", + "uploading": "Augšupielādē", "uploading_media": "Augšupielādē failus", "url": "URL", "usage": "Lietojums", "use_biometric": "Izmantot biometrisko autentifikāciju", + "use_current_connection": "izmantot pašreizējo savienojumu", + "use_custom_date_range": "Izmantot pielāgotu datuma intervālu", "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_privacy": "Lietotāju privātums", "user_purchase_settings": "Iegādāties", "user_purchase_settings_description": "Pirkuma pārvaldība", "user_usage_detail": "Informācija par lietotāju lietojumu", + "user_usage_stats": "Konta izmantošanas statistika", "user_usage_stats_description": "Skatīt konta lietojuma statistiku", "username": "Lietotājvārds", "users": "Lietotāji", "utilities": "Rīki", + "validate": "Pārbaudīt", "variables": "Mainīgie", "version": "Versija", "version_announcement_closing": "Tavs draugs, Alekss", @@ -1297,6 +1439,7 @@ "view_next_asset": "Skatīt nākamo failu", "view_previous_asset": "Skatīt iepriekšējo failu", "view_qr_code": "Skatīt QR kodu", + "view_similar_photos": "Skatīt līdzīgas fotogrāfijas", "view_stack": "Apskatīt kaudzi", "view_user": "Apskatīt lietotāju", "viewer_remove_from_stack": "Noņemt no Steka", diff --git a/i18n/mk.json b/i18n/mk.json index 938d2158da..8430ae117e 100644 --- a/i18n/mk.json +++ b/i18n/mk.json @@ -4,6 +4,7 @@ "account_settings": "Поставки за профилот", "acknowledge": "Прочитано", "action": "Акција", + "action_common_update": "Ажурирај", "actions": "Акции", "active": "Активни", "activity": "Активност", @@ -13,6 +14,8 @@ "add_a_location": "Додади локација", "add_a_name": "Додади име", "add_a_title": "Додади наслов", + "add_birthday": "Додади роденден", + "add_endpoint": "Додади крајна точка", "add_exclusion_pattern": "Додади шаблон за исклучување", "add_import_path": "Додади патека за импортирање", "add_location": "Додади локација", @@ -20,8 +23,15 @@ "add_partner": "Додади партнер", "add_path": "Додади патека", "add_photos": "Додади слики", + "add_tag": "Додади ознака", "add_to": "Додади во…", "add_to_album": "Додади во албум", + "add_to_album_bottom_sheet_added": "Додадено во {album}", + "add_to_album_bottom_sheet_already_exists": "Веќе во {album}", + "add_to_album_bottom_sheet_some_local_assets": "Некои локални ресурси не можеа да се додадат во албумот", + "add_to_album_toggle": "Промени ја селекцијата за {album}", + "add_to_albums": "Додади во албуми", + "add_to_albums_count": "Додади во албуми ({count})", "add_to_shared_album": "Додади во споделен албум", "add_url": "Додади URL", "added_to_archive": "Додадено во архива", @@ -29,17 +39,25 @@ "added_to_favorites_count": "Додадени {count, number} во омилени", "admin": { "add_exclusion_pattern_description": "Додади шаблони за исклучување. Поддржано е користење на glob со *, **, и ?. За да се игнорираат сите датотеки во кој било директориум именуван \"Raw\", користи \"**/Raw/**\". За да се игнорираат сите датотеки што завршуваат со \".tif\", користи \"**/*.tif\". За да се игнорира апсолутна патека, користи \"/path/to/ignore/**\".", + "admin_user": "Административен Корисник", "asset_offline_description": "Ова средство од екстерна библиотека веќе не е пронајдено на дискот и е преместено во ѓубре. Ако датотеката била преместена во рамките на библиотеката, проверете ја вашата временска линија за новото соодветно средство. За да го вратите ова средство, осигурајте се дека долунаведената патека може да биде пристапена од Immich и скенирајте ја библиотеката.", "authentication_settings": "Поставки за автентикација", "authentication_settings_description": "Управувај со лозинки, OAuth, и други поставки за автентикација", "authentication_settings_disable_all": "Дали сте сигурни дека сакате да ги исклучите сите методи за најава? Целосно ќе биде оневозможено најавување.", "authentication_settings_reenable": "За повторно да овозможите, искористете Сервер команда.", "background_task_job": "Позадински задачи", - "backup_database": "Резервна копија од базата на податоци", + "backup_database": "Креирај резервна копија од базата на податоци", "backup_database_enable_description": "Овозможи резервни копии од базата на податоци", "backup_keep_last_amount": "Количина на претходни резервни копии за чување", - "backup_settings": "Поставки за резервни копии", - "backup_settings_description": "Управувај со поставки за резервни копии на базата на податоци", + "backup_onboarding_1_description": "надворешна копија во облакот или на друга физичка локација.", + "backup_onboarding_2_description": "локални копии на различни уреди. Ова ги вклучува и основните фјалови и резервна копија од истите фајлови локално.", + "backup_onboarding_3_description": "сите копии од твоите податоци, вклучувајќи и оргиналните фајлови. Ова вклучува и 1 надворешна копија и 2 локални копии.", + "backup_onboarding_description": "3-2-1 стратегија за резервна копија е препорачано за да ги заштити твоите податоци. Потребно е да чуваш резервни копии од твоите прикачени фотографии/видеа како и базата за податоци на Immich за целосно решение за зачувување на резервна копија", + "backup_onboarding_footer": "Повеќе информации околу правење резервни копии за Immich, ве молам да се референцирате на документацијата", + "backup_onboarding_parts_title": "3-2-1 резервна копија вклучува:", + "backup_onboarding_title": "Резервни копии", + "backup_settings": "Поставки извезување база на податоци", + "backup_settings_description": "Управувај со поставки за извезување на базата на податоци", "cleared_jobs": "Исчистени задачи за: {job}", "config_set_by_file": "Конгигурацијата е моментално поставена од конфигурациска датотека", "confirm_delete_library": "Дали сте сигурни дека сакате да ја избришете библиотеката {library}?", @@ -47,19 +65,40 @@ "confirm_email_below": "За да потврдите, внесете \"{email}\" доле", "confirm_reprocess_all_faces": "Дали сте сигурни дека сакате да се обработат одново сите лица? Ова ќе ги избрише и сите именувани луѓе.", "confirm_user_password_reset": "Дали сте сигурни дека сакате да се поништи лозинката на {user}?", + "confirm_user_pin_code_reset": "Дали сигурно сакаш да го смените ПИН кодот за {user}", "create_job": "Создади задача", "cron_expression": "Cron израз", "cron_expression_description": "Подеси го интервалот на скенирање користејќи го cron форматот. За повеќе информации погледнете на пр. Crontab Guru", "cron_expression_presets": "Предефинирани Cron изрази", "disable_login": "Оневозможи најава", "duplicate_detection_job_description": "Пушти машинско учење на средствата за да се откријат слични слики. Се потпира на Smart Search", + "external_library_management": "Менаџмент на Надворешна Библиотека", + "face_detection": "Детекција на лице", "force_delete_user_warning": "ПРЕДУПРЕДУВАЊЕ: Ова веднаш ќе го отстрани корисникот и сите средства. Оваа акција не може да се поништи и датотеките нема да може да се вратат назад.", "image_format": "Формат", + "image_format_description": "WebP создава помали фајлви отколку JPEG, но е по спор при енкодирање.", + "image_fullsize_enabled": "Овозможи целосна-големина на генерирање на слика", + "image_fullsize_quality_description": "Целосна-големина на слика со квалитет од 1-100. Повисокто е подобро, но создава поголеми фајлови.", + "image_fullsize_title": "Поставки за Целосна-големина на Слика", + "image_prefer_embedded_preview": "Претпочитан вграден преглед", + "image_preview_title": "Поставки за Преглед", "image_quality": "Квалитет", "image_resolution": "Резолуција", "image_settings": "Поставки за слики", + "job_concurrency": "{job} конкурентност", + "job_created": "Креирана задача", + "job_not_concurrency_safe": "Оваа задача не е конкуретно-безбедна.", + "job_settings": "Поставки за задача", + "job_settings_description": "Управувај со конкурентност на задачи", + "job_status": "Статус на задачи", + "library_created": "Креирана библиотека: {library}", + "library_deleted": "Библиотеката е избришана", + "library_import_path_description": "Предложи папка за внес. Оваа папка, вклучува и под папки, ќе биде скенирана за слики и видеа.", "library_scanning": "Периодично скенирање", + "library_scanning_description": "Подеси периодично скениранје на библиотеката", + "library_scanning_enable_description": "Овозможи периодично скениранје на библиотеката", "library_settings": "Екстерна библиотека", + "library_settings_description": "Управувај со подесувањата за надворешната библиотека", "logging_enable_description": "Вклучи евидентирање", "logging_settings": "Евидентирање", "map_dark_style": "Темен стил", @@ -77,7 +116,8 @@ "system_settings": "Системски поставки", "thumbnail_generation_job": "Генерирај сликички", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_threads": "Нишки" + "transcoding_threads": "Нишки", + "transcoding_tone_mapping": "Тонско мапирање" }, "admin_email": "Администрациска Е-пошта", "admin_password": "Администрациска лозинка", @@ -93,11 +133,15 @@ "asset_hashing": "Хеширање…", "asset_offline": "Средството е офлајн", "asset_skipped": "Пропуштено", + "asset_uploaded": "Прикачено", + "asset_uploading": "Прикачување…", "assets": "Средства", "authorized_devices": "Авторизирани уреди", "back": "Назад", + "backup": "Резервна копија", "backward": "Наназад", "blurred_background": "Заматена позадина", + "build": "Верзија", "camera": "Камера", "camera_brand": "Марка на камера", "camera_model": "Модел на камера", @@ -162,15 +206,18 @@ "enabled": "Овозможено", "end_date": "Краен датум", "error": "Грешка", + "exif": "Exif", "expand_all": "Прошири ги сите", "expire_after": "Да истече после", "expired": "Истечено", "explore": "Истражи", + "explorer": "Прегледувач", "export": "Извези", "extension": "Екстензија", "external": "Екстерно", "external_libraries": "Екстерни библиотеки", "face_unassigned": "Недоделено", + "failed": "Неуспешно", "favorite": "Омилено", "favorites": "Омилени", "features": "Функии", @@ -229,8 +276,10 @@ "no_results": "Нема резултати", "notes": "Белешки", "notifications": "Нотификации", + "oauth": "OAuth", "offline": "Офлајн", "ok": "Ок", + "onboarding": "Воведување", "online": "Онлајн", "options": "Опции", "or": "или", diff --git a/i18n/ml.json b/i18n/ml.json index 8787367bb6..1ef0b46e66 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -13,6 +13,7 @@ "add_a_location": "ഒരു സ്ഥലം ചേർക്കുക", "add_a_name": "ഒരു പേര് ചേർക്കുക", "add_a_title": "ഒരു ശീർഷകം ചേർക്കുക", + "add_birthday": "ജന്മദിനം ചേർക്കുക", "add_endpoint": "എൻഡ്പോയിന്റ് ചേർക്കുക", "add_exclusion_pattern": "ഒഴിവാക്കാനുള്ള മാതൃക ചേർക്കുക", "add_import_path": "ഇറക്കുമതി ചെയ്യുക", @@ -22,10 +23,12 @@ "add_path": "പാത ചേര്‍ക്കുക", "add_photos": "ചിത്രങ്ങള്‍ ചേര്‍ക്കുക", "add_tag": "ടാഗ് ചേര്‍ക്കുക", - "add_to": "ചേര്‍ക്കുക", + "add_to": "ചേര്‍ക്കുക…", "add_to_album": "ആല്‍ബത്തിലേക്ക് ചേര്‍ക്കുക", "add_to_album_bottom_sheet_added": "{album} - ലേക്ക് ചേര്‍ത്തു", "add_to_album_bottom_sheet_already_exists": "{album} ആൽബത്തിൽ ഇപ്പോള്‍ തന്നെ ഉണ്ട്", + "add_to_albums": "ആൽബങ്ങളിൽ ചേർക്കുക", + "add_to_albums_count": "ആൽബങ്ങളിൽ ചേർക്കുക ({count})", "add_to_shared_album": "പങ്കിട്ട ആൽബത്തിലേക്ക് ചേർക്കുക", "add_url": "URL ചേര്‍ക്കുക", "added_to_archive": "ചരിത്രരേഖയായി (ആര്‍ക്കൈവ്) ചേര്‍ത്തിരിക്കുന്നു", @@ -68,6 +71,17 @@ "image_format": "ഘടന", "image_format_description": "WebP ഉണ്ടാക്കാന്‍ സമയം എടുക്കും എങ്കിലും JPEG ഫയലുകളെക്കാള്‍ ചെറുതായിരിക്കും.", "image_fullsize_description": "അധികവിവരങ്ങള്‍ ഒഴിവാക്കിയ ചിത്രം, വലുതാക്കി കാണിക്കുമ്പോള്‍ ഉപയോഗിക്കപ്പെടുന്നു", - "image_fullsize_enabled": "പൂര്‍ണ വലുപ്പത്തില്‍ ഉള്ള ചിത്രങ്ങള്‍ ഉണ്ടാക്കാന്‍പ്രാപ്തമാക്കുക" - } + "image_fullsize_enabled": "പൂര്‍ണ വലുപ്പത്തില്‍ ഉള്ള ചിത്രങ്ങള്‍ ഉണ്ടാക്കാന്‍പ്രാപ്തമാക്കുക", + "image_fullsize_quality_description": "1 മുതൽ 100 വരെയുള്ള പൂർണ്ണ വലുപ്പത്തിലുള്ള ഇമേജ് നിലവാരം. ഉയർന്നതാണ് നല്ലത്, പക്ഷേ വലിയ ഫയലുകൾ ഉത്പാദിപ്പിക്കുന്നു.", + "image_fullsize_title": "പൂർണ്ണ വലുപ്പത്തിലുള്ള ഇമേജ് ക്രമീകരണങ്ങൾ", + "image_quality": "ഗുണനിലവാരം", + "job_created": "ജോലി സൃഷ്ടിച്ചു", + "job_status": "ജോലി നില" + }, + "waiting": "കാത്തിരിക്കുന്നു", + "warning": "മുന്നറിയിപ്പ്", + "week": "ആഴ്ച", + "welcome": "സ്വാഗതം", + "year": "വർഷം", + "yes": "അതെ" } diff --git a/i18n/mr.json b/i18n/mr.json index 1af1428d6a..dbc2e3d114 100644 --- a/i18n/mr.json +++ b/i18n/mr.json @@ -28,6 +28,8 @@ "add_to_album": "संग्रहात टाका", "add_to_album_bottom_sheet_added": "{album} मध्ये जोडले गेले", "add_to_album_bottom_sheet_already_exists": "आधीच {album} मध्ये आहे", + "add_to_album_toggle": "अल्बमसाठी निवड टॉगल करा", + "add_to_albums": "अल्बममध्ये जोडा", "add_to_shared_album": "सामायिक संग्रहात टाका", "add_url": "URL प्रविष्ट करा", "added_to_archive": "संग्रहित केले", @@ -381,8 +383,6 @@ "admin_password": "प्रशासक पासवर्ड", "administration": "प्रशासन", "advanced": "प्रगत", - "advanced_settings_beta_timeline_subtitle": "नवीन ॲप अनुभव वापरून पहा", - "advanced_settings_beta_timeline_title": "बीटा टाईमलाईन", "advanced_settings_enable_alternate_media_filter_subtitle": "सिंक दरम्यान वैकल्पिक निकषांवर आधारित मीडिया फिल्टर करण्यासाठी हा पर्याय वापरा. ॲप सर्व अल्बम ओळखण्यात समस्या येत असल्यासच वापरा.", "advanced_settings_enable_alternate_media_filter_title": "[प्रयोगात्मक] उपकरण-आधारित अल्बम सिंक फिल्टर वापरा", "advanced_settings_log_level_title": "लॉग पातळी: {level}", @@ -572,8 +572,6 @@ "backup_setting_subtitle": "बॅकग्राउंड आणि फोरग्राउंड अपलोड सेटिंग्ज व्यवस्थापित करा", "backup_settings_subtitle": "अपलोड सेटिंग्ज व्यवस्थापित करा", "backward": "मागासलेले", - "beta_sync": "बीटा सिंक स्थिती", - "beta_sync_subtitle": "नवीन सिंक प्रणाली व्यवस्थापित करा", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण चालू आहे", "biometric_locked_out": "आपण बायोमेट्रिक प्रमाणीकरणापासून लॉक आहात", "biometric_no_options": "कोणतेही बायोमेट्रिक पर्याय उपलब्ध नाहीत", @@ -1056,6 +1054,706 @@ "group_no": "गटबद्ध नाही", "group_owner": "मालकानुसार गट करा", "group_places_by": "स्थळे गटबद्ध करा: …", + "notification_permission_dialog_content": "सूचना सक्षम करण्यासाठी सेटिंग्जमध्ये जा आणि अनुमती द्या.", + "notification_permission_list_tile_content": "सूचना सक्षम करण्यासाठी परवानगी द्या.", + "notification_permission_list_tile_enable_button": "सूचना सक्षम करा", + "notification_permission_list_tile_title": "सूचना परवानगी", + "notification_toggle_setting_description": "ईमेल सूचना सक्षम करा", + "notifications": "सूचना", + "notifications_setting_description": "सूचना व्यवस्थापित करा", + "oauth": "OAuth", + "official_immich_resources": "अधिकृत Immich संसाधने", + "offline": "ऑफलाइन", + "offset": "ऑफसेट", + "ok": "ठीक", + "oldest_first": "सर्वात जुने आधी", + "on_this_device": "या डिव्हाइसवर", + "onboarding": "ऑनबोर्डिंग", + "onboarding_locale_description": "तुमची पसंतीची भाषा निवडा. हे नंतर सेटिंग्जमध्ये बदलू शकता.", + "onboarding_privacy_description": "खालील (पर्यायी) वैशिष्ट्ये बाह्य सेवांवर अवलंबून आहेत आणि सेटिंग्जमध्ये कधीही अक्षम करता येतात.", + "onboarding_server_welcome_description": "काही सामान्य सेटिंग्जसह तुमची इन्स्टन्स सेटअप करूया.", + "onboarding_theme_description": "तुमच्या इन्स्टन्ससाठी रंग थीम निवडा. हे नंतर सेटिंग्जमध्ये बदलू शकता.", + "onboarding_user_welcome_description": "चला, सुरुवात करूया!", + "onboarding_welcome_user": "स्वागत आहे, {user}", + "online": "ऑनलाइन", + "only_favorites": "फक्त आवडते", + "open": "उघडा", + "open_in_map_view": "नकाशा दृश्यात उघडा", + "open_in_openstreetmap": "OpenStreetMap मध्ये उघडा", + "open_the_search_filters": "शोध फिल्टर उघडा", + "options": "पर्याय", + "or": "किंवा", + "organize_into_albums": "अल्बममध्ये आयोजित करा", + "organize_into_albums_description": "सध्याच्या समक्रमण सेटिंग्ज वापरून विद्यमान फोटो अल्बममध्ये ठेवा", + "organize_your_library": "तुमची लायब्ररी व्यवस्थित करा", + "original": "मूळ", + "other": "इतर", + "other_devices": "इतर उपकरणे", + "other_entities": "इतर घटक", + "other_variables": "इतर चल", + "owned": "मालकीचे", + "owner": "मालक", + "partner": "भागीदार", + "partner_can_access": "{partner} ला प्रवेश आहे", + "partner_can_access_assets": "संग्रहित व हटविलेले वगळता तुमचे सर्व फोटो आणि व्हिडिओ", + "partner_can_access_location": "ज्या ठिकाणी तुमचे फोटो काढले गेले ते स्थान", + "partner_list_user_photos": "{user} चे फोटो", + "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": "{partner} आता तुमचे फोटो पाहू शकणार नाही.", + "partner_sharing": "भागीदार शेअरिंग", + "partners": "भागीदार", + "password": "पासवर्ड", + "password_does_not_match": "पासवर्ड जुळत नाही", + "password_required": "पासवर्ड आवश्यक", + "password_reset_success": "पासवर्ड रीसेट यशस्वी", + "past_durations": { + "days": "मागील {days, plural, one {# दिवस} other {# दिवस}}", + "hours": "मागील {hours, plural, one {# तास} other {# तास}}", + "years": "मागील {years, plural, one {# वर्ष} other {# वर्षे}}" + }, + "path": "मार्ग", + "pattern": "नमुना", + "pause": "थांबवा", + "pause_memories": "आठवणी थांबवा", + "paused": "थांबवले", + "pending": "प्रलंबित", + "people": "लोक", + "people_edits_count": "संपादित {count, plural, one {# व्यक्ती} other {# लोक}}", + "people_feature_description": "लोकांनुसार गटबद्ध फोटो आणि व्हिडिओ ब्राउझ करा", + "people_sidebar_description": "साइडबारमध्ये “लोक” साठी दुवा दाखवा", + "permanent_deletion_warning": "कायमस्वरूपी विलोपन सूचना", + "permanent_deletion_warning_setting_description": "अ‍ॅसेट्स कायमचे हटवताना सूचना दाखवा", + "permanently_delete": "कायमचे हटवा", + "permanently_delete_assets_count": "{count, plural, one {अ‍ॅसेट} other {अ‍ॅसेट्स}} कायमचे हटवा", + "permanently_delete_assets_prompt": "आपण {count, plural, one {हा अ‍ॅसेट कायमचा हटवू इच्छिता?} other {हे अ‍ॅसेट्स कायमचे हटवू इच्छिता?}} यामुळे {count, plural, one {तो त्याच्या} other {ते त्यांच्या}} अल्बम(मधून) देखील काढले जातील.", + "permanently_deleted_asset": "कायमचा हटवलेला अ‍ॅसेट", + "permanently_deleted_assets_count": "कायमचे हटवले {count, plural, one {# अ‍ॅसेट} other {# अ‍ॅसेट्स}}", + "permission": "परवानगी", + "permission_empty": "तुमची परवानगी रिक्त असू नये", + "permission_onboarding_back": "मागे", + "permission_onboarding_continue_anyway": "तरीही पुढे जा", + "permission_onboarding_get_started": "सुरू करा", + "permission_onboarding_go_to_settings": "सेटिंग्जमध्ये जा", + "permission_onboarding_permission_denied": "परवानगी नाकारली. Immich वापरण्यासाठी, सेटिंग्जमध्ये फोटो आणि व्हिडिओ परवानग्या द्या.", + "permission_onboarding_permission_granted": "परवानगी मंजूर! सर्व तयार.", + "permission_onboarding_permission_limited": "परवानगी मर्यादित. Immich ला संपूर्ण गॅलरी संग्रहाचा बॅकअप व व्यवस्थापन करण्यासाठी, सेटिंग्जमध्ये फोटो आणि व्हिडिओ परवानग्या द्या.", + "permission_onboarding_request": "तुमचे फोटो आणि व्हिडिओ पाहण्यासाठी Immich ला परवानगी आवश्यक आहे.", + "person": "व्यक्ती", + "person_age_months": "{months, plural, one {# महिना} other {# महिने}} वय", + "person_age_year_months": "1 वर्ष, {months, plural, one {# महिना} other {# महिने}} वय", + "person_age_years": "{years, plural, other {# वर्षांचे}}", + "person_birthdate": "जन्म {date} रोजी", + "person_hidden": "{name}{hidden, select, true { {hidden}} other {}}", + "photo_shared_all_users": "तुम्ही सर्व वापरकर्त्यांसोबत फोटो शेअर केले आहेत असे दिसते किंवा शेअर करण्यासाठी कोणताही वापरकर्ता नाही.", + "photos": "फोटो", + "photos_and_videos": "फोटो आणि व्हिडिओ", + "photos_count": "{count, plural, one {{count, number} फोटो} other {{count, number} फोटो}}", + "photos_from_previous_years": "मागील वर्षांतील फोटो", + "pick_a_location": "स्थान निवडा", + "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} स्थाने}}", + "play": "प्ले करा", + "play_memories": "आठवणी प्ले करा", + "play_motion_photo": "मोशन फोटो प्ले करा", + "play_or_pause_video": "व्हिडिओ प्ले किंवा पॉज करा", + "please_auth_to_access": "प्रवेशासाठी कृपया प्रमाणीकरण करा", + "port": "पोर्ट", + "preferences_settings_subtitle": "अ‍ॅपची प्राधान्ये व्यवस्थापित करा", + "preferences_settings_title": "प्राधान्ये", + "preset": "प्रिसेट", + "preview": "पूर्वावलोकन", + "previous": "मागील", + "previous_memory": "मागील आठवण", + "previous_or_next_day": "दिवस पुढे/मागे", + "previous_or_next_month": "महिना पुढे/मागे", + "previous_or_next_photo": "फोटो पुढे/मागे", + "previous_or_next_year": "वर्ष पुढे/मागे", + "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": "गिटहब", + "profile_drawer_readonly_mode": "फक्त-वाचन मोड सक्षम. बाहेर पडण्यासाठी वापरकर्त्याच्या अवतार आयकॉनवर लांब-प्रेस करा.", + "profile_drawer_server_out_of_date_major": "सर्व्हर कालबाह्य आहे. कृपया नवीनतम मेजर आवृत्तीवर अद्यतन करा.", + "profile_drawer_server_out_of_date_minor": "सर्व्हर कालबाह्य आहे. कृपया नवीनतम माइनर आवृत्तीवर अद्यतन करा.", + "profile_image_of_user": "{user} ची प्रोफाइल प्रतिमा", + "profile_picture_set": "प्रोफाइल चित्र सेट केले.", + "public_album": "सार्वजनिक अल्बम", + "public_share": "सार्वजनिक शेअर", + "purchase_account_info": "समर्थक", + "purchase_activated_subtitle": "Immich आणि मुक्त-स्रोत सॉफ्टवेअरला पाठिंबा दिल्याबद्दल धन्यवाद", + "purchase_activated_time": "{date} रोजी सक्रिय केले", + "purchase_activated_title": "तुमची की यशस्वीपणे सक्रिय करण्यात आली आहे", + "purchase_button_activate": "सक्रिय करा", + "purchase_button_buy": "खरेदी करा", + "purchase_button_buy_immich": "Immich खरेदी करा", + "purchase_button_never_show_again": "पुन्हा दाखवू नका", + "purchase_button_reminder": "३० दिवसांनी मला आठवण करून द्या", + "purchase_button_remove_key": "की हटवा", + "purchase_button_select": "निवडा", + "purchase_failed_activation": "सक्रिय करण्यात अयशस्वी! योग्य प्रोडक्ट कीसाठी कृपया तुमचे ईमेल तपासा!", + "purchase_individual_description_1": "वैयक्तिक वापरासाठी", + "purchase_individual_description_2": "समर्थक स्थिती", + "purchase_individual_title": "वैयक्तिक", + "purchase_input_suggestion": "प्रॉडक्ट की आहे? खाली की टाका", + "purchase_license_subtitle": "सेवेच्या पुढील विकासासाठी Immich खरेदी करून साथ द्या", + "purchase_lifetime_description": "आयुष्यभराची खरेदी", + "purchase_option_title": "खरेदी पर्याय", + "purchase_panel_info_1": "Immich तयार करणे वेळखाऊ आणि कष्टाचे आहे. आमचे ध्येय मुक्त-स्रोत सॉफ्टवेअर व नैतिक व्यावसायिक पद्धतींमधून टिकाऊ उत्पन्न मिळवणे, विकसकांना आधार देणे आणि शोषणकारी क्लाउड सेवांना पर्याय देणारे गोपनीयतेचा मान राखणारे इकोसिस्टम तयार करणे हे आहे.", + "purchase_panel_info_2": "आम्ही पेवॉल न वाढवण्यास कटिबद्ध आहोत; त्यामुळे या खरेदीमुळे Immich मध्ये कोणतीही अतिरिक्त वैशिष्ट्ये उघडणार नाहीत. चालू विकासासाठी आम्ही तुमच्यासारख्या वापरकर्त्यांच्या पाठबळावर अवलंबून आहोत.", + "purchase_panel_title": "प्रकल्पाला साथ द्या", + "purchase_per_server": "प्रति सर्व्हर", + "purchase_per_user": "प्रति वापरकर्ता", + "purchase_remove_product_key": "प्रॉडक्ट की काढा", + "purchase_remove_product_key_prompt": "तुम्हाला नक्की प्रॉडक्ट की काढायची आहे का?", + "purchase_remove_server_product_key": "सर्व्हरची प्रॉडक्ट की काढा", + "purchase_remove_server_product_key_prompt": "तुम्हाला नक्की सर्व्हरची प्रॉडक्ट की काढायची आहे का?", + "purchase_server_description_1": "संपूर्ण सर्व्हरसाठी", + "purchase_server_description_2": "समर्थक स्थिती", + "purchase_server_title": "सर्व्हर", + "purchase_settings_server_activated": "सर्व्हरची प्रॉडक्ट की प्रशासकाद्वारे व्यवस्थापित केली जाते", + "query_asset_id": "अॅसेट ID चौकशी", + "queue_status": "रांगेत {count}/{total}", + "rating": "स्टार रेटिंग", + "rating_clear": "रेटिंग साफ करा", + "rating_count": "{count, plural, one {# तारा} other {# तारे}}", + "rating_description": "माहिती पॅनेलमध्ये EXIF रेटिंग दर्शवा", + "reaction_options": "रिऍक्शन पर्याय", + "read_changelog": "चेंजलॉग वाचा", + "readonly_mode_disabled": "फक्त-वाचन मोड निष्क्रिय केला", + "readonly_mode_enabled": "फक्त-वाचन मोड सक्षम केला", + "reassign": "पुन्हा नियुक्त करा", + "reassigned_assets_to_existing_person": "{count, plural, one {# आयटम} other {# आयटम}} {name, select, null {विद्यमान व्यक्तीकडे} other {{name} कडे}} पुन्हा नियुक्त केले", + "reassigned_assets_to_new_person": "{count, plural, one {# आयटम} other {# आयटम}} नव्या व्यक्तीकडे पुन्हा नियुक्त केले", + "reassing_hint": "निवडलेले आयटम विद्यमान व्यक्तीकडे नियुक्त करा", + "recent": "अलीकडील", + "recent-albums": "अलीकडील अल्बम", + "recent_searches": "अलीकडील शोध", + "recently_added": "नुकतेच जोडलेले", + "recently_added_page_title": "नुकतेच जोडलेले", + "recently_taken": "अलीकडे घेतलेले", + "recently_taken_page_title": "अलीकडे घेतलेले", + "refresh": "रीफ्रेश करा", + "refresh_encoded_videos": "एन्कोड केलेले व्हिडिओ रीफ्रेश करा", + "refresh_faces": "चेहरे रीफ्रेश करा", + "refresh_metadata": "मेटाडेटा रीफ्रेश करा", + "refresh_thumbnails": "थंबनेल रीफ्रेश करा", + "refreshed": "रीफ्रेश झाले", + "refreshes_every_file": "विद्यमान व नवीन सर्व फाइल्स पुन्हा वाचा", + "refreshing_encoded_video": "एन्कोड केलेला व्हिडिओ रीफ्रेश करत आहे", + "refreshing_faces": "चेहरे रीफ्रेश करत आहे", + "refreshing_metadata": "मेटाडेटा रीफ्रेश करत आहे", + "regenerating_thumbnails": "थंबनेल्स पुन्हा तयार करत आहे", + "remote": "दूरस्थ", + "remote_assets": "दूरस्थ आयटम", + "remove": "काढा", + "remove_assets_album_confirmation": "अल्बममधून {count, plural, one {# आयटम} other {# आयटम}} काढायचे आहेत का?", + "remove_assets_shared_link_confirmation": "या शेअर्ड दुव्यातून {count, plural, one {# आयटम} other {# आयटम}} काढायचे आहेत का?", + "remove_assets_title": "आयटम काढायचे?", + "remove_custom_date_range": "सानुकूल दिनांक श्रेणी काढा", + "remove_deleted_assets": "हटवलेले आयटम काढा", + "remove_from_album": "अल्बममधून काढा", + "remove_from_album_action_prompt": "अल्बममधून {count} काढले", + "remove_from_favorites": "आवडीतून काढा", + "remove_from_lock_folder_action_prompt": "लॉक केलेल्या फोल्डरमधून {count} काढले", + "remove_from_locked_folder": "लॉक फोल्डरमधून काढा", + "remove_from_locked_folder_confirmation": "हे फोटो आणि व्हिडिओ लॉक फोल्डरमधून बाहेर हलवायचे आहेत का? ते तुमच्या लायब्ररीमध्ये दिसतील.", + "remove_from_shared_link": "शेअर्ड दुव्यातून काढा", + "remove_memory": "मेमरी काढा", + "remove_photo_from_memory": "या मेमरीतून फोटो काढा", + "remove_tag": "टॅग काढा", + "remove_url": "URL काढा", + "remove_user": "वापरकर्ता काढा", + "removed_api_key": "काढलेली API की: {name}", + "removed_from_archive": "आर्काइव्हमधून काढले", + "removed_from_favorites": "आवडीतून काढले", + "removed_from_favorites_count": "{count, plural, other {आवडीतून # काढले}}", + "removed_memory": "मेमरी काढली", + "removed_photo_from_memory": "मेमरीतून फोटो काढला", + "removed_tagged_assets": "{count, plural, one {# आयटमवरून टॅग काढला} other {# आयटमवरून टॅग काढले}}", + "rename": "नाव बदला", + "repair": "दुरुस्ती", + "repair_no_results_message": "अनट्रॅक्ड व हरवलेल्या फाइल्स येथे दिसतील", + "replace_with_upload": "अपलोडने बदला", + "repository": "रिपॉझिटरी", + "require_password": "पासवर्ड आवश्यक", + "require_user_to_change_password_on_first_login": "पहिल्या लॉगिनवेळी वापरकर्त्याने पासवर्ड बदलणे आवश्यक", + "rescan": "पुन्हा स्कॅन करा", + "reset": "रीसेट करा", + "reset_password": "पासवर्ड रीसेट करा", + "reset_people_visibility": "लोकांची दृश्यता रीसेट करा", + "reset_pin_code": "PIN कोड रीसेट करा", + "reset_pin_code_description": "तुमचा PIN विसरला असल्यास, तो रीसेट करण्यासाठी सर्व्हर प्रशासकाशी संपर्क साधा", + "reset_pin_code_success": "PIN कोड यशस्वीरीत्या रीसेट केला", + "reset_pin_code_with_password": "पासवर्डने तुम्ही नेहमी PIN कोड रीसेट करू शकता", + "reset_sqlite": "SQLite डेटाबेस रीसेट करा", + "reset_sqlite_confirmation": "तुम्हाला नक्की SQLite डेटाबेस रीसेट करायचा आहे का? डेटा पुन्हा समक्रमित करण्यासाठी तुम्हाला लॉगआउट करून पुन्हा लॉगइन करावे लागेल", + "reset_sqlite_success": "SQLite डेटाबेस यशस्वीरीत्या रीसेट केला", + "reset_to_default": "डीफॉल्टवर रीसेट करा", + "resolve_duplicates": "डुप्लिकेट्स सोडवा", + "resolved_all_duplicates": "सर्व डुप्लिकेट्स सोडवले", + "restore": "पुनर्संचयित करा", + "restore_all": "सर्व पुनर्संचयित करा", + "restore_trash_action_prompt": "कचरापेटीतून {count} पुनर्संचयित केले", + "restore_user": "वापरकर्ता पुनर्संचयित करा", + "restored_asset": "पुनर्संचयित आयटम", + "resume": "पुन्हा सुरू करा", + "resume_paused_jobs": "{count, plural, one {# थांबवलेले काम} other {# थांबवलेली कामे}} पुन्हा सुरू करा", + "retry_upload": "अपलोड पुन्हा करा", + "review_duplicates": "डुप्लिकेट्सचे पुनरावलोकन करा", + "review_large_files": "मोठ्या फाइल्सचे पुनरावलोकन करा", + "role": "भूमिका", + "role_editor": "संपादक", + "role_viewer": "दर्शक", + "running": "चालू", + "save": "जतन करा", + "save_to_gallery": "गॅलरीमध्ये जतन करा", + "saved_api_key": "जतन केलेली API की", + "saved_profile": "जतन केलेले प्रोफाइल", + "saved_settings": "जतन केलेल्या सेटिंग्ज", + "say_something": "काहीतरी बोला", + "scaffold_body_error_occurred": "त्रुटी आली", + "scan_all_libraries": "सर्व लायब्ररी स्कॅन करा", + "scan_library": "स्कॅन करा", + "scan_settings": "स्कॅन सेटिंग्ज", + "scanning_for_album": "अल्बमसाठी स्कॅन करत आहे...", + "search": "शोधा", + "search_albums": "अल्बम शोधा", + "search_by_context": "परिस्थितीनुसार शोधा", + "search_by_description": "वर्णनानुसार शोधा", + "search_by_description_example": "सापा मधील हायकिंगचा दिवस", + "search_by_filename": "फाइल नाव/एक्स्टेंशननुसार शोधा", + "search_by_filename_example": "उदा. IMG_1234.JPG किंवा PNG", + "search_camera_make": "कॅमेरा निर्माता शोधा...", + "search_camera_model": "कॅमेरा मॉडेल शोधा...", + "search_city": "शहर शोधा...", + "search_country": "देश शोधा...", + "search_filter_apply": "फिल्टर लागू करा", + "search_filter_camera_title": "कॅमेरा प्रकार निवडा", + "search_filter_date": "तारीख", + "search_filter_date_interval": "{start} ते {end}", + "search_filter_date_title": "दिनांक श्रेणी निवडा", + "search_filter_display_option_not_in_album": "अल्बममध्ये नाही", + "search_filter_display_options": "प्रदर्शन पर्याय", + "search_filter_filename": "फाइल नावाने शोधा", + "search_filter_location": "स्थान", + "search_filter_location_title": "स्थान निवडा", + "search_filter_media_type": "माध्यम प्रकार", + "search_filter_media_type_title": "माध्यम प्रकार निवडा", + "search_filter_people_title": "लोक निवडा", + "search_for": "यासाठी शोधा", + "search_for_existing_person": "विद्यमान व्यक्ती शोधा", + "search_no_more_result": "आणखी परिणाम नाहीत", + "search_no_people": "कोणतीही व्यक्ती नाही", + "search_no_people_named": "“{name}” नावाची व्यक्ती सापडली नाही", + "search_no_result": "काहीही सापडले नाही; वेगळा शोध शब्द किंवा संयोजन वापरा", + "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_page_selfies": "सेल्फीज", + "search_page_things": "वस्तू", + "search_page_view_all_button": "सर्व पहा", + "search_page_your_activity": "तुमचे क्रियाकलाप", + "search_page_your_map": "तुमचा नकाशा", + "search_people": "लोक शोधा", + "search_places": "ठिकाणे शोधा", + "search_rating": "रेटिंगनुसार शोधा...", + "search_result_page_new_search_hint": "नवीन शोध", + "search_settings": "शोध सेटिंग्ज", + "search_state": "राज्य/स्टेट शोधा...", + "search_suggestion_list_smart_search_hint_1": "डीफॉल्टने स्मार्ट सर्च सुरू आहे; मेटाडेटा शोधण्यासाठी ही रचना वापरा. ", + "search_suggestion_list_smart_search_hint_2": "m:तुमचा-शोध-शब्द", + "search_tags": "टॅग्स शोधा...", + "search_timezone": "वेळक्षेत्र शोधा...", + "search_type": "शोध प्रकार", + "search_your_photos": "तुमचे फोटो शोधा", + "searching_locales": "लोकल्स शोधत आहे...", + "second": "सेकंद", + "see_all_people": "सर्व लोक पाहा", + "select": "निवडा", + "select_album_cover": "अल्बम कव्हर निवडा", + "select_all": "सर्व निवडा", + "select_all_duplicates": "सर्व डुप्लिकेट्स निवडा", + "select_all_in": "{group} मधील सर्व निवडा", + "select_avatar_color": "अवतारचा रंग निवडा", + "select_face": "चेहरा निवडा", + "select_featured_photo": "फिचर्ड फोटो निवडा", + "select_from_computer": "कॉम्प्युटरमधून निवडा", + "select_keep_all": "सर्व ठेवणे निवडा", + "select_library_owner": "लायब्ररी मालक निवडा", + "select_new_face": "नवा चेहरा निवडा", + "select_person_to_tag": "टॅग करण्यासाठी व्यक्ती निवडा", + "select_photos": "फोटो निवडा", + "select_trash_all": "कचरापेटीतील सर्व निवडा", + "select_user_for_sharing_page_err_album": "अल्बम तयार करण्यात अयशस्वी", + "selected": "निवडलेले", + "selected_count": "{count, plural, other {# निवडले}}", + "selected_gps_coordinates": "निवडलेल्या GPS स्थाननिर्देशांक", + "send_message": "संदेश पाठवा", + "send_welcome_email": "स्वागत ईमेल पाठवा", + "server_endpoint": "सर्व्हर एंडपॉइंट", + "server_info_box_app_version": "अॅप आवृत्ती", + "server_info_box_server_url": "सर्व्हर URL", + "server_offline": "सर्व्हर ऑफलाइन", + "server_online": "सर्व्हर ऑनलाइन", + "server_privacy": "सर्व्हर गोपनीयता", + "server_stats": "सर्व्हर आकडेवारी", + "server_version": "सर्व्हर आवृत्ती", + "set": "सेट करा", + "set_as_album_cover": "अल्बम कव्हर म्हणून सेट करा", + "set_as_featured_photo": "फिचर्ड फोटो म्हणून सेट करा", + "set_as_profile_picture": "प्रोफाइल फोटो म्हणून सेट करा", + "set_date_of_birth": "जन्मतारीख सेट करा", + "set_profile_picture": "प्रोफाइल फोटो सेट करा", + "set_slideshow_to_fullscreen": "स्लाइडशो फुलस्क्रीन करा", + "set_stack_primary_asset": "मुख्य आयटम म्हणून सेट करा", + "setting_image_viewer_help": "डीटेल व्ह्यूअर आधी लहान थंबनेल लोड करतो, नंतर (सक्षम असल्यास) मध्यम आकाराचे प्रिव्ह्यू लोड करतो, आणि शेवटी (सक्षम असल्यास) मूळ प्रतिमा लोड करतो.", + "setting_image_viewer_original_subtitle": "पूर्ण-रिझोल्यूशनची मूळ प्रतिमा लोड करण्यासाठी सक्षम करा (मोठी). डेटा वापर कमी करण्यासाठी (नेटवर्क व डिव्हाइस कॅश दोन्ही) अक्षम करा.", + "setting_image_viewer_original_title": "मूळ प्रतिमा लोड करा", + "setting_image_viewer_preview_subtitle": "मध्यम-रिझोल्यूशन प्रतिमा लोड करण्यासाठी सक्षम करा. अक्षम केल्यास थेट मूळ प्रतिमा लोड होईल किंवा फक्त थंबनेल वापरला जाईल.", + "setting_image_viewer_preview_title": "प्रिव्ह्यू प्रतिमा लोड करा", + "setting_image_viewer_title": "प्रतिमा", + "setting_languages_apply": "लागू करा", + "setting_languages_subtitle": "अॅपची भाषा बदला", + "setting_notifications_notify_failures_grace_period": "पार्श्वभूमी बॅकअप अपयशांची सूचना: {duration}", + "setting_notifications_notify_hours": "{count} तास", + "setting_notifications_notify_immediately": "तत्काळ", + "setting_notifications_notify_minutes": "{count} मिनिटे", + "setting_notifications_notify_never": "कधीच नाही", + "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": "सर्व्हरवरून व्हिडिओ स्ट्रिम करताना ट्रान्सकोड उपलब्ध असला तरी मूळ व्हिडिओ प्ले करा. बफरिंग होऊ शकते. स्थानिकरीत्या उपलब्ध व्हिडिओ या सेटिंगपासून स्वतंत्रपणे मूळ गुणवत्तेत प्ले होतात.", + "setting_video_viewer_original_video_title": "मूळ व्हिडिओ सक्तीने प्ले करा", + "settings": "सेटिंग्ज", + "settings_require_restart": "ही सेटिंग लागू करण्यासाठी कृपया Immich रीस्टार्ट करा", + "settings_saved": "सेटिंग्ज जतन केल्या", + "setup_pin_code": "PIN कोड सेट करा", + "share": "शेअर करा", + "share_action_prompt": "{count} आयटम शेअर केले", + "share_add_photos": "फोटो जोडा", + "share_assets_selected": "{count} निवडले", + "share_dialog_preparing": "तयार करत आहे...", + "share_link": "शेअर दुवा", + "shared": "शेअर केले", + "shared_album_activities_input_disable": "टिप्पणी निष्क्रिय आहे", + "shared_album_activity_remove_content": "ही कृती हटवायची आहे का?", + "shared_album_activity_remove_title": "कृती हटवा", + "shared_album_section_people_action_error": "अल्बममधून बाहेर पडताना/काढताना त्रुटी", + "shared_album_section_people_action_leave": "अल्बममधून वापरकर्ता काढा", + "shared_album_section_people_action_remove_user": "अल्बममधून वापरकर्ता काढा", + "shared_album_section_people_title": "लोक", + "shared_by": "यांनी शेअर केले", + "shared_by_user": "{user} यांनी शेअर केले", + "shared_by_you": "तुमच्याकडून शेअर केले", + "shared_from_partner": "{partner} कडील फोटो", + "shared_intent_upload_button_progress_text": "{current}/{total} अपलोड झाले", + "shared_link_app_bar_title": "शेअर्ड दुवे", + "shared_link_clipboard_copied_massage": "क्लिपबोर्डवर कॉपी केले", + "shared_link_clipboard_text": "दुवा: {link}\nपासवर्ड: {password}", + "shared_link_create_error": "शेअर्ड दुवा तयार करताना त्रुटी", + "shared_link_custom_url_description": "सानुकूल URL द्वारे हा शेअर्ड दुवा उघडा", + "shared_link_edit_description_hint": "शेअरचे वर्णन प्रविष्ट करा", + "shared_link_edit_expire_after_option_day": "1 दिवस", + "shared_link_edit_expire_after_option_days": "{count} दिवस", + "shared_link_edit_expire_after_option_hour": "1 तास", + "shared_link_edit_expire_after_option_hours": "{count} तास", + "shared_link_edit_expire_after_option_minute": "1 मिनिट", + "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": "{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": "{count} सेकंदात संपेल", + "shared_link_expires_seconds": "{count} सेकंदात संपेल", + "shared_link_individual_shared": "वैयक्तिक शेअर", + "shared_link_info_chip_metadata": "EXIF (एक्सिफ)", + "shared_link_manage_links": "शेअर्ड दुवे व्यवस्थापित करा", + "shared_link_options": "शेअर्ड दुवा पर्याय", + "shared_link_password_description": "हा शेअर्ड दुवा पाहण्यासाठी पासवर्ड आवश्यक आहे", + "shared_links": "शेअर्ड दुवे", + "shared_links_description": "दुव्याद्वारे फोटो आणि व्हिडिओ शेअर करा", + "shared_photos_and_videos_count": "{assetCount, plural, other {# शेअर्ड फोटो आणि व्हिडिओ}}", + "shared_with_me": "माझ्यासोबत शेअर केलेले", + "shared_with_partner": "{partner} सोबत शेअर केले", + "sharing": "शेअरिंग", + "sharing_enter_password": "हे पृष्ठ पाहण्यासाठी कृपया पासवर्ड प्रविष्ट करा.", + "sharing_page_album": "शेअर्ड अल्बम", + "sharing_page_description": "तुमच्या नेटवर्कमधील लोकांसोबत फोटो-व्हिडिओ शेअर करण्यासाठी शेअर्ड अल्बम तयार करा.", + "sharing_page_empty_list": "रिकामी यादी", + "sharing_sidebar_description": "साइडबारमध्ये शेअरिंगचा दुवा दाखवा", + "sharing_silver_appbar_create_shared_album": "नवीन शेअर्ड अल्बम", + "sharing_silver_appbar_share_partner": "भागीदारासोबत शेअर करा", + "shift_to_permanent_delete": "अॅसेट कायमचे हटवण्यासाठी ⇧ दाबा", + "show_album_options": "अल्बम पर्याय दाखवा", + "show_albums": "अल्बम दाखवा", + "show_all_people": "सर्व लोक दाखवा", + "show_and_hide_people": "लोक दाखवा आणि लपवा", + "show_file_location": "फाइलचे स्थान दाखवा", + "show_gallery": "गॅलरी दाखवा", + "show_hidden_people": "लपवलेले लोक दाखवा", + "show_in_timeline": "टाइमलाइनमध्ये दाखवा", + "show_in_timeline_setting_description": "या वापरकर्त्याचे फोटो-व्हिडिओ तुमच्या टाइमलाइनमध्ये दाखवा", + "show_keyboard_shortcuts": "कीबोर्ड शॉर्टकट दाखवा", + "show_metadata": "मेटाडेटा दाखवा", + "show_or_hide_info": "माहिती दाखवा किंवा लपवा", + "show_password": "पासवर्ड दाखवा", + "show_person_options": "व्यक्तीचे पर्याय दाखवा", + "show_progress_bar": "प्रगती पट्टी दाखवा", + "show_search_options": "शोध पर्याय दाखवा", + "show_shared_links": "शेअर केलेले दुवे दाखवा", + "show_slideshow_transition": "स्लाइडशो ट्रांझिशन दाखवा", + "show_supporter_badge": "समर्थक बॅज", + "show_supporter_badge_description": "समर्थक बॅज दाखवा", + "shuffle": "शफल", + "sidebar": "साइडबार", + "sidebar_display_description": "साइडबारमध्ये दृश्याचा दुवा दाखवा", + "sign_out": "साइन आउट", + "sign_up": "साइन अप", + "size": "आकार", + "skip_to_content": "सामग्रीकडे जा", + "skip_to_folders": "फोल्डर्सकडे जा", + "skip_to_tags": "टॅग्सकडे जा", + "slideshow": "स्लाइडशो", + "slideshow_settings": "स्लाइडशो सेटिंग्ज", + "sort_albums_by": "अल्बम यानुसार क्रम लावा…", + "sort_created": "तयार केलेली तारीख", + "sort_items": "आयटमांची संख्या", + "sort_modified": "बदल केलेली तारीख", + "sort_newest": "अलीकडचा फोटो", + "sort_oldest": "सर्वात जुना फोटो", + "sort_people_by_similarity": "साम्यतेनुसार व्यक्तींचा क्रम लावा", + "sort_recent": "नुकताच घेतलेला फोटो", + "sort_title": "शीर्षक", + "source": "स्त्रोत", + "stack": "स्टॅक", + "stack_action_prompt": "{count} स्टॅक केले", + "stack_duplicates": "डुप्लिकेट्स स्टॅक करा", + "stack_select_one_photo": "स्टॅकसाठी एक मुख्य फोटो निवडा", + "stack_selected_photos": "निवडलेले फोटो स्टॅक करा", + "stacked_assets_count": "स्टॅक केलेले {count, plural, one {# आयटम} other {# आयटम}}", + "stacktrace": "स्टॅकट्रेस", + "start": "सुरू करा", + "start_date": "सुरुवातीची तारीख", + "state": "स्थिती", + "status": "स्टेटस", + "stop_casting": "कास्टिंग थांबवा", + "stop_motion_photo": "मोशन फोटो थांबवा", + "stop_photo_sharing": "तुमचे फोटो शेअर करणे थांबवायचे?", + "stop_photo_sharing_description": "{partner} यांना आता तुमचे फोटो पाहता येणार नाहीत.", + "stop_sharing_photos_with_user": "या वापरकर्त्यासोबत तुमचे फोटो शेअर करणे थांबवा", + "storage": "संचयन जागा", + "storage_label": "संचयन लेबल", + "storage_quota": "संचयन कोटा", + "storage_usage": "{available} पैकी {used} वापरले", + "submit": "सादर करा", + "success": "यशस्वी", + "suggestions": "सूचना", + "sunrise_on_the_beach": "समुद्रकिनाऱ्यावर सूर्योदय", + "support": "सहाय्य", + "support_and_feedback": "सहाय्य आणि अभिप्राय", + "support_third_party_description": "तुमची Immich स्थापना तृतीय-पक्ष पॅकेजद्वारे दिली आहे. तुम्हाला येणाऱ्या समस्या त्या पॅकेजमुळे असू शकतात; त्यामुळे खालील दुव्यांचा वापर करून सर्वप्रथम त्यांच्याकडे समस्या नोंदवा.", + "swap_merge_direction": "मर्ज दिशेची अदलाबदल करा", + "sync": "समक्रमण", + "sync_albums": "अल्बम समक्रमित करा", + "sync_albums_manual_subtitle": "अपलोड केलेले सर्व फोटो-व्हिडिओ निवडलेल्या बॅकअप अल्बममध्ये समक्रमित करा", + "sync_local": "स्थानिक समक्रमण", + "sync_remote": "दूरस्थ समक्रमण", + "sync_status": "समक्रमण स्थिती", + "sync_status_subtitle": "समक्रमण प्रणाली पाहा आणि व्यवस्थापित करा", + "sync_upload_album_setting_subtitle": "Immich वरील निवडलेल्या अल्बममध्ये तुमचे फोटो व व्हिडिओ तयार करा आणि अपलोड करा", + "tag": "टॅग", + "tag_assets": "आयटमना टॅग लावा", + "tag_created": "तयार केलेला टॅग: {tag}", + "tag_feature_description": "तार्किक टॅग विषयांनुसार गटबद्ध फोटो व व्हिडिओ ब्राउझ करा", + "tag_not_found_question": "टॅग सापडत नाही? नवा टॅग तयार करा", + "tag_people": "व्यक्तींना टॅग करा", + "tag_updated": "अद्ययावत टॅग: {tag}", + "tagged_assets": "टॅग केलेले {count, plural, one {# आयटम} other {# आयटम}}", + "tags": "टॅग्स", + "tap_to_run_job": "जॉब चालवण्यासाठी टॅप करा", + "template": "टेम्पलेट", + "theme": "थीम", + "theme_selection": "थीम निवड", + "theme_selection_description": "ब्राउझरच्या सिस्टम पसंतीनुसार थीम आपोआप लाइट/डार्क करा", + "theme_setting_asset_list_storage_indicator_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": "डीटेल इमेज व्ह्यूअरची गुणवत्ता समायोजित करा", + "theme_setting_image_viewer_quality_title": "इमेज व्ह्यूअर गुणवत्ता", + "theme_setting_primary_color_subtitle": "प्राथमिक कृती व अॅक्सेंटसाठी रंग निवडा.", + "theme_setting_primary_color_title": "प्राथमिक रंग", + "theme_setting_system_primary_color_title": "सिस्टम रंग वापरा", + "theme_setting_system_theme_switch": "स्वयंचलित (सिस्टम सेटिंग्जनुसार)", + "theme_setting_theme_subtitle": "अॅपची थीम सेटिंग निवडा", + "theme_setting_three_stage_loading_subtitle": "थ्री-स्टेज लोडिंगमुळे गती वाढू शकते; परंतु नेटवर्क लोड लक्षणीय वाढतो", + "theme_setting_three_stage_loading_title": "थ्री-स्टेज लोडिंग सुरू करा", + "they_will_be_merged_together": "ते एकत्र विलीन केले जातील", + "third_party_resources": "तृतीय-पक्ष संसाधने", + "time_based_memories": "वेळ-आधारित मेमरीज", + "timeline": "टाइमलाइन", + "timezone": "वेळक्षेत्र", + "to_archive": "आर्काइव्ह करा", + "to_change_password": "परवलीचा शब्द बदला", + "to_favorite": "आवडीमध्ये जोडा", + "to_login": "लॉग इन करा", + "to_multi_select": "बहु-निवड करा", + "to_parent": "पालकाकडे जा", + "to_select": "निवडा", + "to_trash": "कचरापेटीत टाका", + "toggle_settings": "सेटिंग्ज टॉगल करा", + "total": "एकूण", + "total_usage": "एकूण वापर", + "trash": "कचरापेटी", + "trash_action_prompt": "{count} कचरापेटीत हलवले", + "trash_all": "सर्व कचरापेटीत टाका", + "trash_count": "कचरापेटी {count, number}", + "trash_delete_asset": "कचरापेटीत टाका/अॅसेट हटवा", + "trash_emptied": "कचरापेटी रिकामी केली", + "trash_no_results_message": "कचरापेटीत टाकलेले फोटो व व्हिडिओ येथे दिसतील.", + "trash_page_delete_all": "सर्व हटवा", + "trash_page_empty_trash_dialog_content": "कचरापेटी रिकामी करायची का? हे आयटम Immich मधून कायमचे हटवले जातील", + "trash_page_info": "कचरापेटीतील आयटम {days} दिवसांनंतर कायमचे हटवले जातील", + "trash_page_no_assets": "कचरापेटीत कोणतेही आयटम नाहीत", + "trash_page_restore_all": "सर्व परत आणा", + "trash_page_select_assets_btn": "आयटम निवडा", + "trash_page_title": "कचरापेटी ({count})", + "trashed_items_will_be_permanently_deleted_after": "कचरापेटीतील आयटम {days, plural, one {# दिवसांनंतर} other {# दिवसांनंतर}} कायमचे हटवले जातील.", + "troubleshoot": "समस्या निवारण", + "type": "प्रकार", + "unable_to_change_pin_code": "PIN कोड बदलता येत नाही", + "unable_to_setup_pin_code": "PIN कोड सेट करू शकत नाही", + "unarchive": "अनआर्काइव्ह करा", + "unarchive_action_prompt": "{count} आर्काइव्हमधून काढले", + "unarchived_count": "{count, plural, other {अनआर्काइव्ह #}}", + "undo": "पूर्ववत करा", + "unfavorite": "आवडीतून काढा", + "unfavorite_action_prompt": "{count} आवडीतून काढले", + "unhide_person": "व्यक्ती दर्शवा", + "unknown": "अज्ञात", + "unknown_country": "अज्ञात देश", + "unknown_year": "अज्ञात वर्ष", + "unlimited": "अमर्यादित", + "unlink_motion_video": "मोशन व्हिडिओ अनलिंक करा", + "unlink_oauth": "OAuth अनलिंक करा", + "unlinked_oauth_account": "OAuth खाते अनलिंक केले", + "unmute_memories": "मेमरीज अनम्यूट करा", + "unnamed_album": "नाव नसलेला अल्बम", + "unnamed_album_delete_confirmation": "तुम्हाला हा अल्बम खरंच हटवायचा आहे का?", + "unnamed_share": "नाव नसलेले शेअर", + "unsaved_change": "न साठवलेला बदल", + "unselect_all": "सर्व निवडी रद्द करा", + "unselect_all_duplicates": "सर्व डुप्लिकेट्सची निवड रद्द करा", + "unselect_all_in": "{group} मधील सर्व निवडी रद्द करा", + "unstack": "स्टॅक वेगळा करा", + "unstack_action_prompt": "{count} अनस्टॅक केले", + "unstacked_assets_count": "अनस्टॅक केलेले {count, plural, one {# आयटम} other {# आयटम}}", + "untagged": "टॅग नसलेले", + "up_next": "पुढे", + "update_location_action_prompt": "निवडलेल्या {count} आयटमचे स्थान याने अद्ययावत करा:", + "updated_at": "अद्ययावत केले", + "updated_password": "परवलीचा शब्द अद्ययावत केला", + "upload": "अपलोड", + "upload_action_prompt": "अपलोडसाठी {count} रांगेत", + "upload_concurrency": "अपलोड समांतरता", + "upload_details": "अपलोड तपशील", + "upload_dialog_info": "निवडलेले आयटम सर्व्हरवर बॅकअप करायचे का?", + "upload_dialog_title": "अॅसेट अपलोड करा", + "upload_errors": "अपलोड पूर्ण झाले; {count, plural, one {# त्रुटी} other {# त्रुटी}} आढळल्या. नवीन अपलोड आयटम पाहण्यासाठी पृष्ठ रीफ्रेश करा.", + "upload_finished": "अपलोड पूर्ण", + "upload_progress": "उर्वरित {remaining, number} — प्रक्रिया झालेले {processed, number}/{total, number}", + "upload_skipped_duplicates": "वगळले {count, plural, one {# डुप्लिकेट आयटम} other {# डुप्लिकेट आयटम}}", + "upload_status_duplicates": "डुप्लिकेट", + "upload_status_errors": "त्रुटी", + "upload_status_uploaded": "अपलोड झाले", + "upload_success": "अपलोड यशस्वी. नवीन अपलोड आयटम दिसण्यासाठी पृष्ठ रीफ्रेश करा.", + "upload_to_immich": "Immich वर अपलोड करा ({count})", + "uploading": "अपलोड होत आहे", + "uploading_media": "माध्यमे अपलोड होत आहेत", + "url": "URL", + "usage": "वापर", + "use_biometric": "बायोमेट्रिक वापरा", + "use_current_connection": "सध्याचे कनेक्शन वापरा", + "use_custom_date_range": "याऐवजी सानुकूल दिनांक श्रेणी वापरा", + "user": "वापरकर्ता", + "user_has_been_deleted": "हा वापरकर्ता हटविला गेला आहे.", + "user_id": "वापरकर्ता आयडी", + "user_liked": "{user} यांना {type, select, photo {हा फोटो} video {हा व्हिडिओ} asset {हा आयटम} other {हे}} आवडले", + "user_pin_code_settings": "PIN कोड", + "user_pin_code_settings_description": "तुमचा PIN कोड व्यवस्थापित करा", + "user_privacy": "वापरकर्ता गोपनीयता", + "user_purchase_settings": "खरेदी", + "user_purchase_settings_description": "तुमची खरेदी व्यवस्थापित करा", + "user_role_set": "{user} यांना {role} म्हणून सेट करा", + "user_usage_detail": "वापरकर्त्याच्या वापराचा तपशील", + "user_usage_stats": "खात्याच्या वापराच्या सांख्यिकी", + "user_usage_stats_description": "खात्याच्या वापराच्या सांख्यिकी पहा", + "username": "वापरकर्तानाव", + "users": "वापरकर्ते", + "users_added_to_album_count": "अल्बममध्ये {count, plural, one {# वापरकर्ता जोडला} other {# वापरकर्ते जोडले}}", + "utilities": "उपयुक्तता", + "validate": "तपासा", + "validate_endpoint_error": "कृपया वैध URL प्रविष्ट करा", + "variables": "चल", + "version": "आवृत्ती", + "version_announcement_closing": "तुमचा मित्र, अ‍ॅलेक्स", + "version_announcement_message": "नमस्कार! Immich ची नवी आवृत्ती उपलब्ध आहे. तुमची संरचना अद्ययावत आणि बिनचूक राहावी यासाठी कृपया काही वेळ काढून रिलीज नोट्स वाचा, विशेषतः तुम्ही WatchTower किंवा अद्ययावत प्रक्रिया स्वयंचलितपणे हाताळणारी कोणतीही व्यवस्था वापरत असाल तर.", + "version_history": "आवृत्ती इतिहास", + "version_history_item": "{date} रोजी {version} स्थापित केली", + "video": "व्हिडिओ", + "video_hover_setting": "हावर केल्यावर व्हिडिओ थंबनेल प्ले करा", + "video_hover_setting_description": "आयटमवर माऊस नेल्यावर व्हिडिओ थंबनेल प्ले होईल. पर्याय बंद असला तरी प्ले चिन्हावर हावर केल्यास प्लेबॅक सुरू करता येईल.", + "videos": "व्हिडिओ", + "videos_count": "{count, plural, one {# व्हिडिओ} other {# व्हिडिओ}}", + "view": "पहा", + "view_album": "अल्बम पहा", + "view_all": "सर्व पहा", + "view_all_users": "सर्व वापरकर्ते पहा", + "view_details": "तपशील पहा", + "view_in_timeline": "टाइमलाइनमध्ये पहा", + "view_link": "दुवा पहा", + "view_links": "दुवे पहा", + "view_name": "पहा", + "view_next_asset": "पुढील आयटम पहा", + "view_previous_asset": "मागील आयटम पहा", + "view_qr_code": "QR कोड पहा", + "view_similar_photos": "समान फोटो पहा", + "view_stack": "स्टॅक पहा", + "view_user": "वापरकर्ता पहा", + "viewer_remove_from_stack": "स्टॅकमधून काढा", + "viewer_stack_use_as_main_asset": "मुख्य आयटम म्हणून वापरा", + "viewer_unstack": "स्टॅक वेगळा करा", + "visibility_changed": "दृश्यता {count, plural, one {# व्यक्तीसाठी बदलली} other {# व्यक्तींसाठी बदलली}}", "waiting": "प्रतीक्षेत", "warning": "चेतावणी", "week": "आठवडा", diff --git a/i18n/ms.json b/i18n/ms.json index e7e0432a4c..c72b1ff688 100644 --- a/i18n/ms.json +++ b/i18n/ms.json @@ -14,6 +14,7 @@ "add_a_location": "Tambah lokasi", "add_a_name": "Tambah nama", "add_a_title": "Tambah tajuk", + "add_birthday": "Tambah hari jadi", "add_endpoint": "Tambah titik akhir", "add_exclusion_pattern": "Tambahkan corak pengecualian", "add_import_path": "Tambahkan laluan import", @@ -27,6 +28,8 @@ "add_to_album": "Tambah ke album", "add_to_album_bottom_sheet_added": "Dimasukkan ke {album}", "add_to_album_bottom_sheet_already_exists": "Sudah ada di {album}", + "add_to_albums": "Tambah pada album", + "add_to_albums_count": "Tambah pada album ({count})", "add_to_shared_album": "Tambah ke album yang dikongsi", "add_url": "Tambah URL", "added_to_archive": "Tambah ke arkib", @@ -44,6 +47,9 @@ "backup_database": "Buat Salinan Pangkalan Data", "backup_database_enable_description": "Dayakan salinan pangkalan data", "backup_keep_last_amount": "Jumlah salinan pangkalan data sebelumnya untuk disimpan", + "backup_onboarding_1_description": "salinan luar tapak di awan atau di lokasi fizikal lain", + "backup_onboarding_2_description": "salinan tempatan pada peranti yang berbeza. Ini termasuk fail utama dan sandaran fail tersebut secara setempat.", + "backup_onboarding_3_description": "jumlah salinan data anda, termasuk fail asal. Ini termasuk 1 salinan luar tapak dan 2 salinan tempatan.", "backup_settings": "Tetapan Salinan Pangkalan Data", "backup_settings_description": "Urus tetapan salinan pangkalan data.", "cleared_jobs": "Kerja telah dibersihkan untuk: {job}", @@ -373,8 +379,6 @@ "admin_password": "Kata laluan Pentadbir", "administration": "Pentadbiran", "advanced": "Lanjutan", - "advanced_settings_beta_timeline_subtitle": "Cuba pengalaman aplikasi baharu", - "advanced_settings_beta_timeline_title": "Garis masa beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Gunakan pilihan ini untuk menapis media semasa penyegerakan berdasarkan kriteria alternatif. Hanya cuba jika anda menghadapi masalah dengan aplikasi mengesan semua album.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAL] Gunakan penapis penyelarasan album peranti alternatif", "advanced_settings_log_level_title": "Tahap log: {level}", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 99b53eae2f..4162170885 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -122,7 +122,14 @@ "library_watching_settings_description": "Se automatisk etter endrede filer", "logging_enable_description": "Aktiver logging", "logging_level_description": "Hvis aktivert, hvilket loggnivå som skal brukes.", - "logging_settings": "Logger", + "logging_settings": "Loggføring", + "machine_learning_availability_checks": "Tilgjengelighetssjekk", + "machine_learning_availability_checks_description": "Automatisk oppdag og velg tilgjengelige maskinlæring-servere", + "machine_learning_availability_checks_enabled": "Aktiver tilgjengelighetssjekk", + "machine_learning_availability_checks_interval": "Sjekkintervall", + "machine_learning_availability_checks_interval_description": "Interval i millisekunder mellom tilgjengelighetssjekk", + "machine_learning_availability_checks_timeout": "Forespørselstimeout", + "machine_learning_availability_checks_timeout_description": "Tidsavbrudd i millisekunder for tilgjengelighetssjekk", "machine_learning_clip_model": "Clip-modell", "machine_learning_clip_model_description": "Navnet på en CLIP-modell finnes her. Merk at du må kjøre 'Smart Søk'-jobben på nytt for alle bilder etter at du har endret modell.", "machine_learning_duplicate_detection": "Duplikatsøk", @@ -387,8 +394,6 @@ "admin_password": "Administrator Passord", "administration": "Administrasjon", "advanced": "Avansert", - "advanced_settings_beta_timeline_subtitle": "Prøv den nye app opplevelsen", - "advanced_settings_beta_timeline_title": "Beta tidslinje", "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å: {level}", @@ -396,6 +401,8 @@ "advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder", "advanced_settings_proxy_headers_subtitle": "Definer proxy headere som Immich skal benytte ved enhver nettverksrequest", "advanced_settings_proxy_headers_title": "Proxy headere", + "advanced_settings_readonly_mode_subtitle": "Aktiverer skrivebeskyttet modus der bildene bare kan vises. Ting som å velge flere bilder, dele, caste og slette er deaktivert. Aktiver/deaktiver skrivebeskyttet modus via brukerens avatar fra hovedskjermen", + "advanced_settings_readonly_mode_title": "Skrivebeskyttet modus", "advanced_settings_self_signed_ssl_subtitle": "Hopper over SSL sertifikatverifikasjon for server-endepunkt. Påkrevet for selvsignerte sertifikater.", "advanced_settings_self_signed_ssl_title": "Tillat selvsignerte SSL sertifikater", "advanced_settings_sync_remote_deletions_subtitle": "Automatisk slette eller gjenopprette filer på denne enheten hvis den handlingen har blitt gjort på nettsiden", @@ -423,6 +430,7 @@ "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?", "album_search_not_found": "Ingen album ble funnet som traff ditt søk", "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_summary": "Oppsummering av album", "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}", @@ -461,6 +469,7 @@ "app_bar_signout_dialog_title": "Logg ut", "app_settings": "Appinstillinger", "appears_in": "Vises i", + "apply_count": "Bruk ({count, number})", "archive": "Arkiv", "archive_action_prompt": "{count} lagt til i arkivet", "archive_or_unarchive_photo": "Arkiver eller ta ut av arkivet", @@ -493,6 +502,8 @@ "asset_restored_successfully": "Objekt(er) gjenopprettet", "asset_skipped": "Hoppet over", "asset_skipped_in_trash": "I søppelbøtten", + "asset_trashed": "Objekt slettet", + "asset_troubleshoot": "Feilsøk objekt", "asset_uploaded": "Lastet opp", "asset_uploading": "Laster opp…", "asset_viewer_settings_subtitle": "Endre dine visningsinnstillinger for galleriet", @@ -500,7 +511,7 @@ "assets": "Filer", "assets_added_count": "Lagt til {count, plural, one {# objekt} other {# objekter}}", "assets_added_to_album_count": "Lagt til {count, plural, one {# objekter} other {# objekt}} i album", - "assets_added_to_albums_count": "Lagt til {assetTotal, plural, one {# asset} other {# assets}} til {albumTotal} albumer", + "assets_added_to_albums_count": "Lagt til {assetTotal, plural, one {# asset} other {# assets}} til {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Objektet} other {Objektene}} kan ikke legges til i albumet", "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} kan ikke legges til i noen av albumene", "assets_count": "{count, plural, one {# fil} other {# filer}}", @@ -526,8 +537,10 @@ "autoplay_slideshow": "Autoavspilling av lysbildefremvisning", "back": "Tilbake", "back_close_deselect": "Tilbake, lukk eller fjern merking", + "background_backup_running_error": "Bakgrunnsbackup kjører, kan ikke starte manuell backup", "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", + "background_options": "Bakgrunnsinnstillinger", "backup": "Sikkerhetskopiering", "backup_album_selection_page_albums_device": "Album på enhet ({count})", "backup_album_selection_page_albums_tap": "Trykk for å inkludere, dobbelttrykk for å ekskludere", @@ -535,6 +548,7 @@ "backup_album_selection_page_select_albums": "Velg album", "backup_album_selection_page_selection_info": "Valginformasjon", "backup_album_selection_page_total_assets": "Totalt antall unike objekter", + "backup_albums_sync": "Synkronisering av sikkerhetskopialbum", "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…", @@ -594,8 +608,6 @@ "backup_setting_subtitle": "Administrer opplastingsinnstillinger for bakgrunn og forgrunn", "backup_settings_subtitle": "Håndter opplastingsinnstillinger", "backward": "Bakover", - "beta_sync": "Beta synkroniseringsstatus", - "beta_sync_subtitle": "Håndter det nye synkroniseringssystemet", "biometric_auth_enabled": "Biometrisk autentisering aktivert", "biometric_locked_out": "Du er låst ute av biometrisk verifisering", "biometric_no_options": "Ingen biometriske valg tilgjengelige", @@ -653,6 +665,8 @@ "change_pin_code": "Endre PIN kode", "change_your_password": "Endre passordet ditt", "changed_visibility_successfully": "Endret synlighet vellykket", + "charging": "Lading", + "charging_requirement_mobile_backup": "Bakgrunnsbackup krever at enheten lader", "check_corrupt_asset_backup": "Sjekk etter korrupte backupobjekter", "check_corrupt_asset_backup_button": "Utfør sjekk", "check_corrupt_asset_backup_description": "Kjør denne sjekken kun over Wi-Fi og når alle objekter har blitt lastet opp. Denne sjekken kan ta noen minutter.", @@ -739,6 +753,7 @@ "create_user": "Opprett Bruker", "created": "Opprettet", "created_at": "Laget", + "creating_linked_albums": "Oppretter sammenkoblede albumer...", "crop": "Beskjær", "curated_object_page_title": "Ting", "current_device": "Nåværende enhet", @@ -888,7 +903,9 @@ "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_getting_places": "Feil ved henting av steder", "error_loading_image": "Feil ved lasting av bilde", + "error_loading_partners": "Feil ved lasting av partnere: {error}", "error_saving_image": "Feil: {error}", "error_tag_face_bounding_box": "Feil ved merking av ansikt - klarte ikke å få koordinatene på omrisset", "error_title": "Feil - Noe gikk galt", @@ -1053,6 +1070,7 @@ "favorites_page_no_favorites": "Ingen favorittobjekter funnet", "feature_photo_updated": "Fremhevet bilde oppdatert", "features": "Funksjoner", + "features_in_development": "Funksjoner under utvikling", "features_setting_description": "Administrerer funksjoner for appen", "file_name": "Filnavn", "file_name_or_extension": "Filnavn eller filtype", @@ -1073,12 +1091,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funksjonen laster eksterne ressurser fra Google for å fungere.", "general": "Generelt", + "geolocation_instruction_location": "Klikk på et objekt med GPS-koordinater for å bruke posisjonen, eller velg en posisjon direkte fra kartet", "get_help": "Få Hjelp", "get_wifiname_error": "Kunne ikke hente Wi-Fi-navnet. Sørg for at du har gitt de nødvendige tillatelsene og er koblet til et Wi-Fi-nettverk", "getting_started": "Kom i gang", "go_back": "Gå tilbake", "go_to_folder": "Gå til mappe", "go_to_search": "Gå til søk", + "gps": "GPS", + "gps_missing": "Ingen GPS", "grant_permission": "Gi tillatelse", "group_albums_by": "Grupper album etter...", "group_country": "Grupper etter land", @@ -1214,6 +1235,7 @@ "local": "Lokal", "local_asset_cast_failed": "Kan ikke caste et bilde som ikke er lastet opp til serveren", "local_assets": "Lokale objekter", + "local_media_summary": "Oppsummering av lokale media", "local_network": "Lokalt nettverk", "local_network_sheet_info": "Appen vil koble til serveren via denne URL-en når du bruker det angitte Wi-Fi-nettverket", "location_permission": "Stedstillatelse", @@ -1225,6 +1247,7 @@ "location_picker_longitude_hint": "Skriv inn lengdegrad her", "lock": "Lås", "locked_folder": "Låst mappe", + "log_detail_title": "Loggdetaljer", "log_out": "Logg ut", "log_out_all_devices": "Logg ut fra alle enheter", "logged_in_as": "Logget inn som {user}", @@ -1255,6 +1278,7 @@ "login_password_changed_success": "Passord oppdatert", "logout_all_device_confirmation": "Er du sikker på at du vil logge ut av alle enheter?", "logout_this_device_confirmation": "Er du sikker på at du vil logge ut av denne enheten?", + "logs": "Logger", "longitude": "Lengdegrad", "look": "Se", "loop_videos": "Gjenta Videoer", @@ -1262,6 +1286,7 @@ "main_branch_warning": "Du bruker en utviklingsversjon; vi anbefaler på det sterkeste og bruke en utgitt versjon!", "main_menu": "Hovedmeny", "make": "Merke", + "manage_geolocation": "Administrer plassering", "manage_shared_links": "Håndter delte linker", "manage_sharing_with_partners": "Administrer deling med partnere", "manage_the_app_settings": "Administrer appinnstillingene", @@ -1296,6 +1321,7 @@ "mark_as_read": "Merk som lest", "marked_all_as_read": "Merket alle som lest", "matches": "Samsvarende", + "matching_assets": "Matchende objekter", "media_type": "Mediatype", "memories": "Minner", "memories_all_caught_up": "Alt utført", @@ -1336,6 +1362,7 @@ "name_or_nickname": "Navn eller kallenavn", "network_requirement_photos_upload": "Bruk mobildata for backup av bilder", "network_requirement_videos_upload": "Bruk mobildata for backup av videoer", + "network_requirements": "Nettverkskrav", "network_requirements_updated": "Nettverkskrav endret, resetter backupkø", "networking_settings": "Nettverk", "networking_subtitle": "Administrer serverendepunkt-innstillinger", @@ -1346,6 +1373,7 @@ "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_timeline": "Ny tidslinje", "new_user_created": "Ny bruker opprettet", "new_version_available": "NY VERSJON TILGJENGELIG", "newest_first": "Nyeste først", @@ -1359,20 +1387,25 @@ "no_assets_message": "KLIKK FOR Å LASTE OPP DITT FØRSTE BILDE", "no_assets_to_show": "Ingen objekter å vise", "no_cast_devices_found": "Ingen caste-enheter oppdaget", + "no_checksum_local": "Ingen sjekksum tilgjengelig - Kan ikke hente lokale objekter", + "no_checksum_remote": "Ingen sjekksum tilgjengelig - Kan ikke hente eksterne objekter", "no_duplicates_found": "Ingen duplikater ble funnet.", "no_exif_info_available": "Ingen EXIF-informasjon tilgjengelig", "no_explore_results_message": "Last opp flere bilder for å utforske samlingen din.", "no_favorites_message": "Legg til favoritter for å finne dine beste bilder og videoer raskt", "no_libraries_message": "Opprett et eksternt bibliotek for å se bildene og videoene dine", + "no_local_assets_found": "Ingen lokale objekter funnet med denne sjekksummen", "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", "no_places": "Ingen steder", + "no_remote_assets_found": "Ingen eksterne objekter funnet med denne sjekksummen", "no_results": "Ingen resultater", "no_results_description": "Prøv et synonym eller mer generelt søkeord", "no_shared_albums_message": "Opprett et album for å dele bilder og videoer med personer i nettverket ditt", "no_uploads_in_progress": "Ingen opplasting pågår", + "not_available": "Ikke tilgjengelig", "not_in_any_album": "Ikke i noe album", "not_selected": "Ikke valgt", "note_apply_storage_label_to_previously_uploaded assets": "Merk: For å bruke lagringsetiketten på tidligere opplastede filer, kjør", @@ -1407,6 +1440,8 @@ "open_the_search_filters": "Åpne søkefiltrene", "options": "Valg", "or": "eller", + "organize_into_albums": "Organiser til albumer", + "organize_into_albums_description": "Plasser eksisterende bilder i albumer ved å bruke synkroniseringsinnstillinger", "organize_your_library": "Organiser biblioteket ditt", "original": "original", "other": "Annet", @@ -1492,6 +1527,7 @@ "port": "Port", "preferences_settings_subtitle": "Administrer appens preferanser", "preferences_settings_title": "Innstillinger", + "preparing": "Forbereder", "preset": "Forhåndsinstilling", "preview": "Forhåndsvis", "previous": "Forrige", @@ -1508,6 +1544,7 @@ "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_readonly_mode": "Skrivebeskyttet modus er aktivert. Langttrykk på brukerens avatarikon for å avslutte.", "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}", @@ -1546,6 +1583,7 @@ "purchase_server_description_2": "Støttespiller status", "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren", + "query_asset_id": "Forespør objektID", "queue_status": "Kø {count}/{total}", "rating": "Stjernevurdering", "rating_clear": "Slett vurdering", @@ -1553,6 +1591,9 @@ "rating_description": "Hvis EXIF vurdering i informasjons panelet", "reaction_options": "Reaksjonsalternativer", "read_changelog": "Les endringslogg", + "readonly_mode_disabled": "Skrivebeskyttet modus deaktivert", + "readonly_mode_enabled": "Skrivebeskyttet modus aktivert", + "ready_for_upload": "Klar for opplasting", "reassign": "Tilordne på nytt", "reassigned_assets_to_existing_person": "Flyttet {count, plural, one {# objekt} other {# objekter}} to {name, select, null {en eksisterende person} other {{name}}}", "reassigned_assets_to_new_person": "Flyttet {count, plural, one {# objekt} other {# objekter}} til en ny person", @@ -1577,6 +1618,7 @@ "regenerating_thumbnails": "Regenererer miniatyrbilder", "remote": "Eksternt", "remote_assets": "Eksterne objekter", + "remote_media_summary": "Oppsummering av eksterne media", "remove": "Fjern", "remove_assets_album_confirmation": "Er du sikker på at du fil slette {count, plural, one {# objekt} other {# objekter}} fra albumet?", "remove_assets_shared_link_confirmation": "Er du sikker på at du vil slette {count, plural, one {# objekt} other {# objekter}} fra den delte lenken?", @@ -1629,6 +1671,7 @@ "restore_user": "Gjenopprett bruker", "restored_asset": "Gjenopprettet ressurs", "resume": "Fortsett", + "resume_paused_jobs": "Fortsett {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Prøv opplasting på nytt", "review_duplicates": "Gjennomgå duplikater", "review_large_files": "Se gjennom store filer", @@ -1722,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Feilet ved oppretting av album", "selected": "Valgt", "selected_count": "{count, plural, other {# valgt}}", + "selected_gps_coordinates": "Valgte GPS-koordinater", "send_message": "Send melding", "send_welcome_email": "Send velkomstmelding", "server_endpoint": "Server endepunkt", @@ -1850,6 +1894,7 @@ "show_slideshow_transition": "Vis overgang til lysbildefremvisning", "show_supporter_badge": "Supportermerke", "show_supporter_badge_description": "Vis et supportermerke", + "show_text_search_menu": "Vis tekstsøk meny", "shuffle": "Bland", "sidebar": "Sidefelt", "sidebar_display_description": "Vis en lenke for visningen i sidefeltet", @@ -1880,6 +1925,7 @@ "stacktrace": "Stakkspor", "start": "Start", "start_date": "Startdato", + "start_date_before_end_date": "Startdato må være før sluttdato", "state": "Fylke", "status": "Status", "stop_casting": "Stopp casting", @@ -1904,6 +1950,8 @@ "sync_albums_manual_subtitle": "Synkroniser alle opplastede videoer og bilder til det valgte backupalbumet", "sync_local": "Synkroniser lokalt", "sync_remote": "Synkroniser eksternt", + "sync_status": "Synkroniseringsstatus", + "sync_status_subtitle": "Vis og håndter synkronisering", "sync_upload_album_setting_subtitle": "Opprett og last opp dine bilder og videoer til det valgte albumet på Immich", "tag": "Tagg", "tag_assets": "Merk ressurser", @@ -1941,7 +1989,9 @@ "to_change_password": "Endre passord", "to_favorite": "Favoritt", "to_login": "Logg inn", + "to_multi_select": "for multivalg", "to_parent": "Gå til overodnet", + "to_select": "for valg", "to_trash": "Papirkurv", "toggle_settings": "Bytt innstillinger", "total": "Total", @@ -1961,6 +2011,7 @@ "trash_page_select_assets_btn": "Velg objekter", "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}}.", + "troubleshoot": "Feilsøk", "type": "Type", "unable_to_change_pin_code": "Klarte ikke å endre PIN-kode", "unable_to_setup_pin_code": "Klarte ikke å sette opp PINkode", @@ -1991,6 +2042,7 @@ "unstacked_assets_count": "Ikke stablet {count, plural, one {# objekt} other {# objekter}}", "untagged": "Umerket", "up_next": "Neste", + "update_location_action_prompt": "Oppdater plasseringen til {count} valgte objekter med:", "updated_at": "Oppdatert", "updated_password": "Passord oppdatert", "upload": "Last opp", @@ -2057,6 +2109,7 @@ "view_next_asset": "Vis neste fil", "view_previous_asset": "Vis forrige fil", "view_qr_code": "Vis QR-kode", + "view_similar_photos": "Vis lignende bilder", "view_stack": "Vis stabel", "view_user": "Vis bruker", "viewer_remove_from_stack": "Fjern fra stabling", @@ -2075,5 +2128,6 @@ "yes": "Ja", "you_dont_have_any_shared_links": "Du har ingen delte lenker", "your_wifi_name": "Ditt Wi-Fi-navn", - "zoom_image": "Zoom Bilde" + "zoom_image": "Zoom Bilde", + "zoom_to_bounds": "Zoom til grensene" } diff --git a/i18n/nl.json b/i18n/nl.json index 51b44b1a5a..82846668d8 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -28,6 +28,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_album_bottom_sheet_some_local_assets": "Sommige lokale items konden niet aan album toegevoegd worden", "add_to_album_toggle": "Selectie inschakelen voor {album}", "add_to_albums": "Toevoegen aan albums", "add_to_albums_count": "Toevoegen aan albums ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Logboek inschakelen", "logging_level_description": "Indien ingeschakeld, welk logniveau er wordt gebruikt.", "logging_settings": "Logging", + "machine_learning_availability_checks": "Beschikbaarheid", + "machine_learning_availability_checks_description": "Automatisch detecteren en selecteren van beschikbare machine learning servers", + "machine_learning_availability_checks_enabled": "Activeer beschikbaarheid controles", + "machine_learning_availability_checks_interval": "Controleinterval", + "machine_learning_availability_checks_interval_description": "Interval in milliseconden tussen beschikbaarheid checks", + "machine_learning_availability_checks_timeout": "Verzoek time-out", + "machine_learning_availability_checks_timeout_description": "Time-out in milliseconden voor beschikbaarheidschecks", "machine_learning_clip_model": "CLIP model", "machine_learning_clip_model_description": "De naam van een CLIP-model dat hier is vermeld. Let op: je moet de 'Slim Zoeken -taak opnieuw uitvoeren voor alle afbeeldingen wanneer je een model wijzigt.", "machine_learning_duplicate_detection": "Duplicaat detectie", @@ -293,7 +301,7 @@ "theme_settings_description": "Beheer het uiterlijk van de Immich webinterface", "thumbnail_generation_job": "Thumbnail genereren", "thumbnail_generation_job_description": "Genereer grote, kleine en vervaagde thumbnails voor ieder item, en genereer thumbnails voor iedere persoon", - "transcoding_acceleration_api": "Acceleration API", + "transcoding_acceleration_api": "Versnelling API", "transcoding_acceleration_api_description": "De API die met je apparaat zal communiceren om transcodering te versnellen. Deze instelling is 'best effort': wanneer fouten optreden wordt teruggevallen op softwaretranscodering. VP9 kan wel of niet werken, afhankelijk van je hardware.", "transcoding_acceleration_nvenc": "NVENC (vereist NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (vereist 7e generatie Intel CPU of nieuwer)", @@ -312,7 +320,7 @@ "transcoding_codecs_learn_more": "Om meer te leren over de terminologie die hier wordt gebruikt, bekijk de FFmpeg documentatie voor H.264 codec, HEVC codec en VP9 codec.", "transcoding_constant_quality_mode": "Constante kwaliteit modus", "transcoding_constant_quality_mode_description": "ICQ is beter dan CQP, maar sommige hardware versnellingsmethodes ondersteunen deze modus niet. Als u deze optie instelt, wordt de voorkeur gegeven aan de opgegeven modus bij gebruik van op kwaliteit gebaseerde encoding. Deze optie wordt genegeerd door NVENC omdat het ICQ niet ondersteunt.", - "transcoding_constant_rate_factor": "Constant rate factor (-crf)", + "transcoding_constant_rate_factor": "Constant tarief factor (-ctf)", "transcoding_constant_rate_factor_description": "Niveau voor videokwaliteit. Typische waarden zijn 23 voor H.264, 28 voor HEVC, 31 voor VP9 en 35 voor AV1. Lager is beter, maar produceert grotere bestanden.", "transcoding_disabled_description": "Transcodeer geen video's. Het afspelen kan op sommige clients niet meer werken", "transcoding_encoding_options": "Coderings Opties", @@ -325,7 +333,7 @@ "transcoding_max_b_frames_description": "Hogere waarden verbeteren de compressie efficiëntie, maar vertragen de codering. Is mogelijk niet compatibel met hardwareversnelling op oudere apparaten. 0 schakelt B-frames uit, terwijl -1 deze waarde automatisch instelt.", "transcoding_max_bitrate": "Maximum bitrate", "transcoding_max_bitrate_description": "Het instellen van een maximale bitrate kan de bestandsgrootte voorspelbaarder maken, tegen geringe kosten voor de kwaliteit. Bij 720p zijn de typische waarden 2600 kbit/s voor VP9 of HEVC, of 4500 kbit/s voor H.264. Uitgeschakeld indien ingesteld op 0.", - "transcoding_max_keyframe_interval": "Maximum keyframe interval", + "transcoding_max_keyframe_interval": "Maximale keyframe interval", "transcoding_max_keyframe_interval_description": "Stelt de maximale frameafstand tussen keyframes in. Lagere waarden verslechteren de compressie efficiëntie, maar verbeteren de zoektijden en kunnen de kwaliteit verbeteren in scènes met snelle bewegingen. 0 stelt deze waarde automatisch in.", "transcoding_optimal_description": "Video's met een hogere resolutie dan de doelresolutie of niet in een geaccepteerd formaat", "transcoding_policy": "Transcode beleid", @@ -341,7 +349,7 @@ "transcoding_settings_description": "Beheer welke videos worden getranscodeerd en hoe ze worden verwerkt", "transcoding_target_resolution": "Target resolutie", "transcoding_target_resolution_description": "Hogere resoluties kunnen meer details behouden, maar het coderen ervan duurt langer, de bestandsgrootte is groter en de app reageert mogelijk minder snel.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "Tijdelijke AQ", "transcoding_temporal_aq_description": "Alleen van toepassing op NVENC. Verhoogt de kwaliteit van scènes met veel details en weinig beweging. Is mogelijk niet compatibel met oudere apparaten.", "transcoding_threads": "Threads", "transcoding_threads_description": "Hogere waarden leiden tot snellere codering, maar laten minder ruimte over voor de server om andere taken te verwerken terwijl deze actief is. Deze waarde mag niet groter zijn dan het aantal CPU cores. Maximaliseert het gebruik als deze is ingesteld op 0.", @@ -387,8 +395,6 @@ "admin_password": "Beheerder wachtwoord", "administration": "Beheer", "advanced": "Geavanceerd", - "advanced_settings_beta_timeline_subtitle": "Probeer de nieuwe app-ervaring", - "advanced_settings_beta_timeline_title": "Beta tijdlijn", "advanced_settings_enable_alternate_media_filter_subtitle": "Gebruik deze optie om media te filteren tijdens de synchronisatie op basis van alternatieve criteria. Gebruik dit enkel als de app problemen heeft met het detecteren van albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTEEL] Gebruik een alternatieve album synchronisatie filter", "advanced_settings_log_level_title": "Logniveau: {level}", @@ -396,6 +402,8 @@ "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_readonly_mode_subtitle": "Schakelt de alleen-lezenmodus in, waarbij de foto's alleen bekeken kunnen worden. Dingen zoals het selecteren van meerdere afbeeldingen, delen, casten en verwijderen zijn allemaal uitgeschakeld. Schakel alleen-lezen in of uit via de gebruikers avatar vanaf het hoofdscherm", + "advanced_settings_readonly_mode_title": "Alleen-lezen Mode", "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", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Weet je zeker dat je {user} wilt verwijderen?", "album_search_not_found": "Geen albums gevonden die aan je zoekopdracht voldoen", "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_summary": "Album samenvatting", "album_updated": "Album bijgewerkt", "album_updated_setting_description": "Ontvang een e-mailmelding wanneer een gedeeld album nieuwe items heeft", "album_user_left": "{album} verlaten", @@ -461,8 +470,9 @@ "app_bar_signout_dialog_title": "Log uit", "app_settings": "App instellingen", "appears_in": "Komt voor in", + "apply_count": "Toepassen ({count, number})", "archive": "Archief", - "archive_action_prompt": "{count} toegevoegd aan archief", + "archive_action_prompt": "{count} item(s) toegevoegd aan het archief", "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen", "archive_page_no_archived_assets": "Geen gearchiveerde items gevonden", "archive_page_title": "Archief ({count})", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Item succesvol hersteld", "asset_skipped": "Overgeslagen", "asset_skipped_in_trash": "In prullenbak", + "asset_trashed": "Asset verwijderd", + "asset_troubleshoot": "Asset probleemoplossing", "asset_uploaded": "Geüpload", "asset_uploading": "Uploaden…", "asset_viewer_settings_subtitle": "Beheer je instellingen voor galerijweergave", @@ -500,7 +512,7 @@ "assets": "Items", "assets_added_count": "{count, plural, one {# item} other {# items}} toegevoegd", "assets_added_to_album_count": "{count, plural, one {# item} other {# items}} aan het album toegevoegd", - "assets_added_to_albums_count": "{assetTotal, plural, one {# asset} other {# assets}} toegevoegd aan {albumTotal} albums", + "assets_added_to_albums_count": "{assetTotal, plural, one {# asset} other {# assets}} toegevoegd aan {albumTotal, plural, one {# album} other {#albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {# item} other {# items}} konden niet aan album toegevoegd worden", "assets_cannot_be_added_to_albums": "{count, plural, one {Middel kan} other {Middelen kunnen}} niet toegevoegd worden aan de albums", "assets_count": "{count, plural, one {# item} other {# items}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Diavoorstelling automatisch afspelen", "back": "Terug", "back_close_deselect": "Terug, sluiten of deselecteren", + "background_backup_running_error": "Achtergrond backup draait, handmatige backup kan niet worden gestart", "background_location_permission": "Achtergrond locatie toestemming", "background_location_permission_content": "Om van netwerk te wisselen terwijl de app op de achtergrond draait, heeft Immich *altijd* toegang tot de exacte locatie nodig om de naam van het WiFi-netwerk te kunnen lezen", + "background_options": "Achtergrond opties", "backup": "Back-up", "backup_album_selection_page_albums_device": "Albums op apparaat ({count})", "backup_album_selection_page_albums_tap": "Tik om op te nemen, dubbel tik om uit te sluiten", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Selecteer albums", "backup_album_selection_page_selection_info": "Selectie info", "backup_album_selection_page_total_assets": "Totaal unieke items", + "backup_albums_sync": "Backup albums synchronisatie", "backup_all": "Alle", "backup_background_service_backup_failed_message": "Fout bij het back-uppen van de items. Opnieuw proberen…", "backup_background_service_connection_failed_message": "Fout bij het verbinden met de server. Opnieuw proberen…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "Beheer achtergrond en voorgrond uploadinstellingen", "backup_settings_subtitle": "Beheer upload instellingen", "backward": "Achteruit", - "beta_sync": "Beta Sync Status", - "beta_sync_subtitle": "Beheer het nieuwe synchronisatiesysteem", "biometric_auth_enabled": "Biometrische authenticatie ingeschakeld", "biometric_locked_out": "Biometrische authenticatie is vergrendeld", "biometric_no_options": "Geen biometrische opties beschikbaar", @@ -653,6 +666,8 @@ "change_pin_code": "Wijzig PIN code", "change_your_password": "Wijzig je wachtwoord", "changed_visibility_successfully": "Zichtbaarheid succesvol gewijzigd", + "charging": "Opladen", + "charging_requirement_mobile_backup": "Achtergrond backup vereist dat het apparaat wordt opgeladen", "check_corrupt_asset_backup": "Controleer op corrupte back-ups van items", "check_corrupt_asset_backup_button": "Controle uitvoeren", "check_corrupt_asset_backup_description": "Voer deze controle alleen uit via WiFi en nadat alle items zijn geback-upt. De procedure kan een paar minuten duren.", @@ -707,7 +722,7 @@ "control_bottom_app_bar_edit_time": "Datum & tijd bewerken", "control_bottom_app_bar_share_link": "Link delen", "control_bottom_app_bar_share_to": "Delen met", - "control_bottom_app_bar_trash_from_immich": "Naar prullenbak", + "control_bottom_app_bar_trash_from_immich": "Verwijderen van Immich", "copied_image_to_clipboard": "Afbeelding gekopieerd naar klembord.", "copied_to_clipboard": "Gekopieerd naar klembord!", "copy_error": "Fout bij kopiëren", @@ -739,6 +754,7 @@ "create_user": "Gebruiker aanmaken", "created": "Aangemaakt", "created_at": "Aangemaakt", + "creating_linked_albums": "Gekoppelde albums worden aangemaakt...", "crop": "Bijsnijden", "curated_object_page_title": "Dingen", "current_device": "Huidig apparaat", @@ -768,7 +784,7 @@ "default_locale_description": "Formatteer datums en getallen op basis van de landinstellingen van je browser", "delete": "Verwijderen", "delete_action_confirmation_message": "Weet je zeker dat je dit item wilt verwijderen? Deze actie zorgt ervoor dat het item naar de prullenbak van de server wordt verplaatst en je wordt gevraagd of je deze ook lokaal wilt verwijderen", - "delete_action_prompt": "{count} verwijderd", + "delete_action_prompt": "{count} item(s) verwijderd", "delete_album": "Album verwijderen", "delete_api_key_prompt": "Weet je zeker dat je deze API-sleutel wilt verwijderen?", "delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat", @@ -782,12 +798,12 @@ "delete_key": "Verwijder key", "delete_library": "Verwijder bibliotheek", "delete_link": "Verwijder link", - "delete_local_action_prompt": "{count} lokaal verwijderd", + "delete_local_action_prompt": "{count} item(s) lokaal verwijderd", "delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up", "delete_local_dialog_ok_force": "Toch verwijderen", "delete_others": "Andere verwijderen", "delete_permanently": "Permanent verwijderen", - "delete_permanently_action_prompt": "{count} permanent verwijderd", + "delete_permanently_action_prompt": "{count} item(s) permanent verwijderd", "delete_shared_link": "Verwijder gedeelde link", "delete_shared_link_dialog_title": "Verwijder gedeelde link", "delete_tag": "Tag verwijderen", @@ -816,7 +832,7 @@ "documentation": "Documentatie", "done": "Klaar", "download": "Downloaden", - "download_action_prompt": "{count} items downloaden", + "download_action_prompt": "{count} item(s) aan het downloaden", "download_canceled": "Download geannuleerd", "download_complete": "Download voltooid", "download_enqueue": "Download in wachtrij", @@ -834,7 +850,7 @@ "download_sucess_android": "Het bestand is gedownload naar DCIM/Immich", "download_waiting_to_retry": "Wachten om opnieuw te proberen", "downloading": "Downloaden", - "downloading_asset_filename": "Item {filename} downloaden...", + "downloading_asset_filename": "Downloaden asset {filename}", "downloading_media": "Media aan het downloaden", "drop_files_to_upload": "Zet bestanden ergens neer om ze te uploaden", "duplicates": "Duplicaten", @@ -846,7 +862,7 @@ "edit_birthday": "Wijzig verjaardag", "edit_date": "Datum bewerken", "edit_date_and_time": "Datum en tijd bewerken", - "edit_date_and_time_action_prompt": "{count} datum en tijd bewerkt", + "edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count} item(s)", "edit_date_and_time_by_offset": "Wijzigen datum door verschuiving", "edit_date_and_time_by_offset_interval": "Nieuw datuminterval: {from}-{to}", "edit_description": "Beschrijving bewerken", @@ -858,7 +874,7 @@ "edit_key": "Key bewerken", "edit_link": "Link bewerken", "edit_location": "Locatie bewerken", - "edit_location_action_prompt": "{count} locatie(s) aangepast", + "edit_location_action_prompt": "Locatie bijgewerkt van {count} item(s)", "edit_location_dialog_title": "Locatie", "edit_name": "Naam bewerken", "edit_people": "Mensen bewerken", @@ -888,7 +904,9 @@ "error": "Fout", "error_change_sort_album": "Sorteervolgorde van album wijzigen mislukt", "error_delete_face": "Fout bij verwijderen van gezicht uit het item", + "error_getting_places": "Fout bij ophalen plaatsen", "error_loading_image": "Fout bij laden afbeelding", + "error_loading_partners": "Fout bij ophalen partners: {error}", "error_saving_image": "Fout: {error}", "error_tag_face_bounding_box": "Fout bij taggen van gezicht - kan coördinaten van omvattend kader niet ophalen", "error_title": "Fout - Er is iets misgegaan", @@ -908,7 +926,7 @@ "error_deleting_shared_user": "Fout bij verwijderen van gedeelde gebruiker", "error_downloading": "Fout bij downloaden {filename}", "error_hiding_buy_button": "Fout bij het verbergen van de koop knop", - "error_removing_assets_from_album": "Fout bij verwijderen van items uit album, controleer de console voor meer details", + "error_removing_assets_from_album": "Fout bij het verwijderen van items uit het album, controleer de console voor meer details", "error_selecting_all_assets": "Fout bij selecteren van alle items", "exclusion_pattern_already_exists": "Dit uitsluitingspatroon bestaat al.", "failed_to_create_album": "Fout bij maken van album", @@ -1047,12 +1065,13 @@ "failed_to_load_assets": "Kan items niet laden", "failed_to_load_folder": "Laden van map mislukt", "favorite": "Favoriet", - "favorite_action_prompt": "{count}toegevoegd aan Favorieten", + "favorite_action_prompt": "{count} item(s) toegevoegd aan je favorieten", "favorite_or_unfavorite_photo": "Foto markeren als of verwijderen uit favorieten", "favorites": "Favorieten", "favorites_page_no_favorites": "Geen favoriete items gevonden", "feature_photo_updated": "Uitgelichte afbeelding bijgewerkt", "features": "Functies", + "features_in_development": "Functies in ontwikkeling", "features_setting_description": "Beheer de app functies", "file_name": "Bestandsnaam", "file_name_or_extension": "Bestandsnaam of extensie", @@ -1073,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Deze functie gebruikt externe bronnen van Google om te kunnen werken.", "general": "Algemeen", + "geolocation_instruction_location": "Klik op een item met GPS coördinaten om de locatie te gebruiken, of selecteer een locatie direct vanaf de kaart", "get_help": "Krijg hulp", "get_wifiname_error": "Kon de WiFi-naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een WiFi-netwerk", "getting_started": "Aan de slag", "go_back": "Ga terug", "go_to_folder": "Ga naar map", "go_to_search": "Ga naar zoeken", + "gps": "GPS", + "gps_missing": "Geen GPS", "grant_permission": "Geef toestemming", "group_albums_by": "Groepeer albums op...", "group_country": "Groepeer op land", @@ -1214,6 +1236,7 @@ "local": "Lokaal", "local_asset_cast_failed": "Kan geen item casten die nog niet geüpload is naar de server", "local_assets": "Lokale Items", + "local_media_summary": "Lokale media samenvatting", "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": "Locatietoestemming", @@ -1225,6 +1248,7 @@ "location_picker_longitude_hint": "Voer hier je lengtegraad in", "lock": "Vergrendel", "locked_folder": "Vergrendelde map", + "log_detail_title": "Log details", "log_out": "Uitloggen", "log_out_all_devices": "Uitloggen op alle apparaten", "logged_in_as": "Ingelogd als {user}", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "Wachtwoord succesvol bijgewerkt", "logout_all_device_confirmation": "Weet je zeker dat je wilt uitloggen op alle apparaten?", "logout_this_device_confirmation": "Weet je zeker dat je wilt uitloggen op dit apparaat?", + "logs": "Logs", "longitude": "Lengtegraad", "look": "Uiterlijk", "loop_videos": "Video's herhalen", @@ -1262,6 +1287,7 @@ "main_branch_warning": "Je gebruikt een ontwikkelingsversie. We raden je ten zeerste aan een releaseversie te gebruiken!", "main_menu": "Hoofdmenu", "make": "Merk", + "manage_geolocation": "Beheer locatie", "manage_shared_links": "Beheer gedeelde links", "manage_sharing_with_partners": "Beheer delen met partners", "manage_the_app_settings": "Beheer de appinstellingen", @@ -1296,6 +1322,7 @@ "mark_as_read": "Markeren als gelezen", "marked_all_as_read": "Allen gemarkeerd als gelezen", "matches": "Overeenkomsten", + "matching_assets": "Overeenkomende assets", "media_type": "Mediatype", "memories": "Herinneringen", "memories_all_caught_up": "Je bent helemaal bij", @@ -1322,7 +1349,7 @@ "more": "Meer", "move": "Verplaats", "move_off_locked_folder": "Verplaats uit vergrendelde map", - "move_to_lock_folder_action_prompt": "{count} toegevoegd aan de vergrendelde map", + "move_to_lock_folder_action_prompt": "{count} item(s) toegevoegd aan de 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 {# item} other {# items}} verplaatst naar archief", @@ -1336,6 +1363,7 @@ "name_or_nickname": "Naam of gebruikersnaam", "network_requirement_photos_upload": "Gebruik mobiele data voor de backup van foto's", "network_requirement_videos_upload": "Gebruik mobiele data voor de backups van video's", + "network_requirements": "Netwerk vereisten", "network_requirements_updated": "Netwerkeisen zijn gewijzigd, back-upwachtrij wordt opnieuw ingesteld", "networking_settings": "Netwerk", "networking_subtitle": "Beheer de instellingen voor de server-URL", @@ -1346,6 +1374,7 @@ "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_timeline": "Nieuwe tijdlijn", "new_user_created": "Nieuwe gebruiker aangemaakt", "new_version_available": "NIEUWE VERSIE BESCHIKBAAR", "newest_first": "Nieuwste eerst", @@ -1359,20 +1388,25 @@ "no_assets_message": "KLIK HIER OM JE EERSTE FOTO TE UPLOADEN", "no_assets_to_show": "Geen foto's om te laten zien", "no_cast_devices_found": "Geen cast-apparaten gevonden", + "no_checksum_local": "Geen checksum beschikbaar - kan lokale assets niet ophalen", + "no_checksum_remote": "Geen checksum beschikbaar - kan online assets niet ophalen", "no_duplicates_found": "Er zijn geen duplicaten gevonden.", "no_exif_info_available": "Geen exif info beschikbaar", "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_local_assets_found": "Geen lokale assets gevonden met deze checksum", "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 meldingen", "no_people_found": "Geen mensen gevonden", "no_places": "Geen plaatsen", + "no_remote_assets_found": "Geen online assets gevonden met deze checksum", "no_results": "Geen resultaten", "no_results_description": "Probeer een synoniem of een algemener zoekwoord", "no_shared_albums_message": "Maak een album om foto's en video's te delen met mensen in je netwerk", "no_uploads_in_progress": "Geen uploads bezig", + "not_available": "N.B.", "not_in_any_album": "Niet in een album", "not_selected": "Niet geselecteerd", "note_apply_storage_label_to_previously_uploaded assets": "Opmerking: om het opslaglabel toe te passen op eerder geüploade items, voer de volgende taak uit", @@ -1407,6 +1441,8 @@ "open_the_search_filters": "Open de zoekfilters", "options": "Opties", "or": "of", + "organize_into_albums": "Organiseren in albums", + "organize_into_albums_description": "Bestaande foto's in albums plaatsen met de huidige synchronisatie-instellingen", "organize_your_library": "Organiseer je bibliotheek", "original": "origineel", "other": "Overige", @@ -1492,6 +1528,7 @@ "port": "Poort", "preferences_settings_subtitle": "Beheer de voorkeuren van de app", "preferences_settings_title": "Voorkeuren", + "preparing": "Voorbereiden", "preset": "Voorinstelling", "preview": "Voorbeeld", "previous": "Vorige", @@ -1503,18 +1540,19 @@ "primary": "Primair", "privacy": "Privacy", "profile": "Profiel", - "profile_drawer_app_logs": "Logboek", + "profile_drawer_app_logs": "Logs", "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_readonly_mode": "Alleen-lezen-modus ingeschakeld. Druk lang op je profielfoto om te verlaten.", "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_account_info": "Ondersteuner", "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", @@ -1546,6 +1584,7 @@ "purchase_server_description_2": "Supporterstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "De licentiesleutel van de server wordt beheerd door de beheerder", + "query_asset_id": "Query Asset ID", "queue_status": "Wachtrij {count}/{total}", "rating": "Sterwaardering", "rating_clear": "Waardering verwijderen", @@ -1553,6 +1592,9 @@ "rating_description": "De EXIF-waardering weergeven in het infopaneel", "reaction_options": "Reactie-opties", "read_changelog": "Lees wijzigingen", + "readonly_mode_disabled": "Alleen-lezen modus uitgeschakeld", + "readonly_mode_enabled": "Alleen-lezen modus ingeschakeld", + "ready_for_upload": "Klaar voor upload", "reassign": "Opnieuw toewijzen", "reassigned_assets_to_existing_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan {name, select, null {een bestaand persoon} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan een nieuw persoon", @@ -1577,16 +1619,17 @@ "regenerating_thumbnails": "Thumbnails opnieuw aan het genereren", "remote": "Externe", "remote_assets": "Externe Items", + "remote_media_summary": "Online media samenvatting", "remove": "Verwijderen", "remove_assets_album_confirmation": "Weet je zeker dat je {count, plural, one {# item} other {# items}} uit het album wilt verwijderen?", "remove_assets_shared_link_confirmation": "Weet je zeker dat je {count, plural, one {# item} other {# items}} uit deze gedeelde link wilt verwijderen?", "remove_assets_title": "Items verwijderen?", "remove_custom_date_range": "Aangepast datumbereik verwijderen", "remove_deleted_assets": "Verwijder offline bestanden", - "remove_from_album": "Verwijder uit album", - "remove_from_album_action_prompt": "{count} verwijderd uit het album", + "remove_from_album": "Verwijderen uit album", + "remove_from_album_action_prompt": "{count} item(s) verwijderd uit het album", "remove_from_favorites": "Verwijderen uit favorieten", - "remove_from_lock_folder_action_prompt": "{count} verwijderd uit de vergrendelde map", + "remove_from_lock_folder_action_prompt": "{count} item(s) verwijderd uit de vergrendelde map", "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", @@ -1625,10 +1668,11 @@ "resolved_all_duplicates": "Alle duplicaten opgelost", "restore": "Herstellen", "restore_all": "Herstel alle", - "restore_trash_action_prompt": "{count} teruggezet uit prullenbak", + "restore_trash_action_prompt": "{count} item(s) teruggehaald uit de prullenbak", "restore_user": "Gebruiker herstellen", "restored_asset": "Item hersteld", "resume": "Hervatten", + "resume_paused_jobs": "Hervat {count, plural, one {# gepauseerde taak} other {# gepauseerde taken}}", "retry_upload": "Opnieuw uploaden", "review_duplicates": "Controleer duplicaten", "review_large_files": "Grote bestanden beoordelen", @@ -1682,7 +1726,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_screenshots": "Schermafbeelding", "search_page_search_photos_videos": "Zoek naar je foto's en video's", "search_page_selfies": "Selfies", "search_page_things": "Dingen", @@ -1722,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Album aanmaken mislukt", "selected": "Geselecteerd", "selected_count": "{count, plural, other {# geselecteerd}}", + "selected_gps_coordinates": "Geselecteerde GPS Coördinaten", "send_message": "Bericht versturen", "send_welcome_email": "Stuur welkomstmail", "server_endpoint": "Server-URL", @@ -1767,9 +1812,9 @@ "settings_saved": "Instellingen opgeslagen", "setup_pin_code": "Stel een PIN code in", "share": "Delen", - "share_action_prompt": "{count} items gedeeld", + "share_action_prompt": "{count} item(s) gedeeld", "share_add_photos": "Foto's toevoegen", - "share_assets_selected": "{count} geselecteerd", + "share_assets_selected": "{count} item(s) geselecteerd", "share_dialog_preparing": "Voorbereiden...", "share_link": "Link delen", "shared": "Gedeeld", @@ -1850,6 +1895,7 @@ "show_slideshow_transition": "Diavoorstellingsovergang tonen", "show_supporter_badge": "Supportersbadge", "show_supporter_badge_description": "Toon een supportersbadge", + "show_text_search_menu": "Laat tekst zoek menu zien", "shuffle": "Willekeurig", "sidebar": "Zijbalk", "sidebar_display_description": "Toon een link naar deze pagina in de zijbalk", @@ -1872,7 +1918,7 @@ "sort_title": "Titel", "source": "Bron", "stack": "Stapel", - "stack_action_prompt": "{count} gestapeld", + "stack_action_prompt": "{count} item(s) gestapeld", "stack_duplicates": "Stapel duplicaten", "stack_select_one_photo": "Selecteer één primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", @@ -1880,6 +1926,7 @@ "stacktrace": "Stacktrace", "start": "Start", "start_date": "Startdatum", + "start_date_before_end_date": "Startdatum moet voor einddatum liggen", "state": "Staat", "status": "Status", "stop_casting": "Stop met casten", @@ -1904,6 +1951,8 @@ "sync_albums_manual_subtitle": "Synchroniseer alle geüploade video’s en foto’s naar de geselecteerde back-up albums", "sync_local": "Lokaal synchroniseren", "sync_remote": "Op afstand synchroniseren", + "sync_status": "Sync Status", + "sync_status_subtitle": "Bekijk en beheer het synchronisatie systeem", "sync_upload_album_setting_subtitle": "Maak en upload je foto's en video's naar de geselecteerde albums op Immich", "tag": "Tag", "tag_assets": "Items taggen", @@ -1941,13 +1990,15 @@ "to_change_password": "Wijzig wachtwoord", "to_favorite": "Toevoegen aan favorieten", "to_login": "Inloggen", + "to_multi_select": "naar multi-select", "to_parent": "Ga naar hoofdmap", + "to_select": "naar selecteren", "to_trash": "Prullenbak", "toggle_settings": "Zichtbaarheid instellingen wisselen", "total": "Totaal", "total_usage": "Totaal gebruik", "trash": "Prullenbak", - "trash_action_prompt": "{count} verwijderd naar de prullenbak", + "trash_action_prompt": "{count} item(s) verplaatst naar de prullenbak", "trash_all": "Verplaats alle naar prullenbak", "trash_count": "{count, number} naar prullenbak", "trash_delete_asset": "Items naar prullenbak verplaatsen of verwijderen", @@ -1961,6 +2012,7 @@ "trash_page_select_assets_btn": "Selecteer items", "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.", + "troubleshoot": "Problemen oplossen", "type": "Type", "unable_to_change_pin_code": "PIN code kan niet gewijzigd worden", "unable_to_setup_pin_code": "PIN code kan niet ingesteld worden", @@ -1969,7 +2021,7 @@ "unarchived_count": "{count, plural, other {# verwijderd uit archief}}", "undo": "Ongedaan maken", "unfavorite": "Verwijderen uit favorieten", - "unfavorite_action_prompt": "{count} verwijderd uit favorieten", + "unfavorite_action_prompt": "{count} verwijderd uit je favorieten", "unhide_person": "Persoon zichtbaar maken", "unknown": "Onbekend", "unknown_country": "Onbekend Land", @@ -1987,14 +2039,15 @@ "unselect_all_duplicates": "Deselecteer alle duplicaten", "unselect_all_in": "Deselecteer alles in {group}", "unstack": "Ontstapelen", - "unstack_action_prompt": "{count} ontstapeld", + "unstack_action_prompt": "{count} item(s) ontstapeld", "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld", "untagged": "Ongemarkeerd", "up_next": "Volgende", + "update_location_action_prompt": "Werk de locatie bij van {count} geselecteerde items met:", "updated_at": "Geüpdatet", "updated_password": "Wachtwoord bijgewerkt", "upload": "Uploaden", - "upload_action_prompt": "{count} in de wachtrij voor uploaden", + "upload_action_prompt": "{count} item(s) staan in de wachtrij voor uploaden", "upload_concurrency": "Aantal gelijktijdige uploads", "upload_details": "Uploaddetails", "upload_dialog_info": "Wil je een backup maken van de geselecteerde item(s) op de server?", @@ -2018,7 +2071,7 @@ "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} item {dit bestand} other {dit}} geliket", + "user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {} other {dit item}} geliket", "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Beheer je PIN code", "user_privacy": "Gebruikersprivacy", @@ -2054,13 +2107,14 @@ "view_link": "Bekijk link", "view_links": "Links bekijken", "view_name": "Bekijken", - "view_next_asset": "Bekijk volgende item", - "view_previous_asset": "Bekijk vorige item", + "view_next_asset": "Bekijk volgend item", + "view_previous_asset": "Bekijk vorig item", "view_qr_code": "QR-code bekijken", + "view_similar_photos": "Bekijk vergelijkbare foto's", "view_stack": "Bekijk stapel", "view_user": "Bekijk gebruiker", - "viewer_remove_from_stack": "Verwijder van Stapel", - "viewer_stack_use_as_main_asset": "Gebruik als Hoofd Item", + "viewer_remove_from_stack": "Verwijder van stapel", + "viewer_stack_use_as_main_asset": "Zet bovenaan de stapel", "viewer_unstack": "Ontstapel", "visibility_changed": "Zichtbaarheid gewijzigd voor {count, plural, one {# persoon} other {# mensen}}", "waiting": "Wachtend", @@ -2075,5 +2129,6 @@ "yes": "Ja", "you_dont_have_any_shared_links": "Je hebt geen gedeelde links", "your_wifi_name": "Je WiFi-naam", - "zoom_image": "Inzoomen" + "zoom_image": "Inzoomen", + "zoom_to_bounds": "Zoom naar randen" } diff --git a/i18n/nn.json b/i18n/nn.json index 01f76f528b..7b2f256c94 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -28,6 +28,8 @@ "add_to_album": "Legg til i album", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allereie i {album}", + "add_to_albums": "Legg til i album", + "add_to_albums_count": "Legg til i album ({count})", "add_to_shared_album": "Legg til i delt album", "add_url": "Legg til URL", "added_to_archive": "Lagt til i arkiv", @@ -45,6 +47,7 @@ "backup_database": "Lag tryggingskopi av database", "backup_database_enable_description": "Aktiver tryggingskopiering av database", "backup_keep_last_amount": "Antal tryggingskopiar å behalde", + "backup_onboarding_1_description": "sikkerheitskopi i skya eller på eit anna fysisk sted.", "backup_onboarding_2_description": "lokale kopiar på andre einingar. Dette inkluderer hovudfilene og backup av desse filene lokalt.", "backup_onboarding_3_description": "fullstendige kopiar av dine data, inkludert originalfilene. Dette inkluderer 1 utomhus kopi og 2 lokale kopiar.", "backup_onboarding_description": "Ein 3-2-1 backup-strategi tilrådast for å verne dataa dine. Du bør ha kopiar av dei opplasta bileta/videoane dine samt Immich-databasen, slik at du har ei fleirdelt backup-løysing.", @@ -78,6 +81,7 @@ "image_format_description": "WebP gjev mindre filstorleik enn JPEG, men er treigare å lage.", "image_fullsize_description": "Bilete i full storleik utan metadata, i bruk når zooma inn", "image_fullsize_enabled": "Skru på generering av bilete i full storleik", + "image_fullsize_enabled_description": "Generer bilete i full storleik for ikkje web-tilpassa formatar. Når \"Foretrekk", "image_fullsize_quality_description": "Kvalitet på bilete i full storleik frå 1-100. Høgare er betre, men gjev større filer.", "image_fullsize_title": "Innstillingar for bilete i full storleik", "image_prefer_embedded_preview": "Bruk helst innebygd førehandsvisning", @@ -118,6 +122,9 @@ "logging_enable_description": "Aktiver loggføring", "logging_level_description": "Når aktivert, kva loggnivå å bruke.", "logging_settings": "Logging", + "machine_learning_availability_checks_description": "Automatiser oppdaging og prioritet av tilgjengelege maskinlærings-serverar", + "machine_learning_availability_checks_interval": "Sjekk intervall", + "machine_learning_availability_checks_timeout_description": "Utløpstid i millisekund for tilgjengelegheitssjekk", "machine_learning_clip_model": "CLIP modell", "machine_learning_clip_model_description": "Namnet på ein CLIP modell finst her. Merk at du må køyre 'Smart Søk'-jobben på nytt for alle bilete etter du har forandra modell.", "machine_learning_duplicate_detection": "Duplikatdeteksjon", @@ -139,6 +146,7 @@ "machine_learning_min_detection_score": "Minimum deteksjonsresultat", "machine_learning_min_detection_score_description": "Minimum tillitspoeng for at eit ansikt skal bli oppdaga, på ein skala frå 0 til 1. Lågare verdiar vil oppdage fleire ansikt, men kan føre til feilaktige treff.", "machine_learning_min_recognized_faces": "Minimum gjenkjende ansikt", + "machine_learning_min_recognized_faces_description": "Minste tal på gjenkjende fjes for å opprette ein person. Aukar ein dette, vert ansiktsgjenkjenninga meir presis, på bekostning av auka sjanse for at ansikt ikkje vert tileigna ein person.", "machine_learning_settings": "Innstillingar for maskinlæring", "machine_learning_settings_description": "Administrer maskinlæringsfunksjonar og innstillingar", "machine_learning_smart_search": "Smart Søk", @@ -154,6 +162,7 @@ "map_settings": "Kart", "map_settings_description": "Endre kartinnstillingar", "map_style_description": "URL til eit style.json-karttema", + "memory_generate_job": "Minne-generering", "metadata_extraction_job": "Hent ut metadata", "metadata_extraction_job_description": "Hent ut metadata frå kvart bilete, slik som GPS, ansikt og oppløysing", "metadata_faces_import_setting": "Skru på import av ansikt", @@ -161,6 +170,17 @@ "metadata_settings": "Metadata Innstillinger", "metadata_settings_description": "Endre metadata-innstillingar", "migration_job": "Migrasjon", + "migration_job_description": "Overfør miniatyrbilete for bilete og ansikt til den nyaste mappestrukturen", + "nightly_tasks_cluster_faces_setting_description": "Køyr ansiktsgjenkjenning på nyleg identifiserte ansikt", + "nightly_tasks_database_cleanup_setting_description": "Fjern gamal, utgått data frå databasen", + "nightly_tasks_generate_memories_setting": "Generer minner", + "nightly_tasks_generate_memories_setting_description": "Lag nye minner frå bilete", + "nightly_tasks_missing_thumbnails_setting": "Generer manglande miniatyrbilete", + "nightly_tasks_missing_thumbnails_setting_description": "Set bilete utan miniatyrbilete i kø for generering av miniatyrbilete", + "nightly_tasks_settings": "Innstillingar for nattlege jobbar", + "nightly_tasks_settings_description": "Handsam nattlege jobbar", + "nightly_tasks_start_time_setting": "Starttid", + "nightly_tasks_start_time_setting_description": "Tidspunktet serveren køyrer nattlege jobbar", "notification_email_from_address": "Frå adresse", "notification_email_test_email_failed": "Mislukka sending av test-e-post, sjekk konfigurasjonen din", "notification_email_test_email_sent": "Det vart sendt ei test-melding til {email}. Sjekk e-posten din.", diff --git a/i18n/pl.json b/i18n/pl.json index 6d42d5ec54..f2016bd1ce 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -28,6 +28,7 @@ "add_to_album": "Dodaj do albumu", "add_to_album_bottom_sheet_added": "Dodano do {album}", "add_to_album_bottom_sheet_already_exists": "Już jest w {album}", + "add_to_album_bottom_sheet_some_local_assets": "Niektóre lokalne zasoby nie mogły zostać dodane do albumu", "add_to_album_toggle": "Przełącz wybieranie dla {album}", "add_to_albums": "Dodaj do albumów", "add_to_albums_count": "Dodaj do albumów ({count})", @@ -44,7 +45,7 @@ "authentication_settings_description": "Zarządzaj hasłem, OAuth i innymi ustawienia uwierzytelnienia", "authentication_settings_disable_all": "Czy jesteś pewny, że chcesz wyłączyć wszystkie metody logowania? Logowanie będzie całkowicie wyłączone.", "authentication_settings_reenable": "Aby ponownie włączyć, użyj Polecenia serwera.", - "background_task_job": "Zadania w Tle", + "background_task_job": "Zadania w tle", "backup_database": "Utwórz Zrzut Bazy Danych", "backup_database_enable_description": "Włącz zrzuty bazy danych", "backup_keep_last_amount": "Ile poprzednich zrzutów przechowywać", @@ -123,6 +124,13 @@ "logging_enable_description": "Uruchom zapisywanie logów", "logging_level_description": "Kiedy włączone, jakiego poziomu użyć.", "logging_settings": "Rejestrowanie logów", + "machine_learning_availability_checks": "Sprawdzanie dostępności", + "machine_learning_availability_checks_description": "Automatyczne wykrywaj i preferuj dostępne serwery uczenia maszynowego", + "machine_learning_availability_checks_enabled": "Włącz sprawdzanie dostępności", + "machine_learning_availability_checks_interval": "Częstotliwość sprawdzania", + "machine_learning_availability_checks_interval_description": "Odstęp czasu w milisekundach między sprawdzeniami dostępności", + "machine_learning_availability_checks_timeout": "Upłynął czas żądania", + "machine_learning_availability_checks_timeout_description": "Limit czasu żądania w milisekundach dla sprawdzania dostępności", "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", @@ -233,7 +241,7 @@ "oauth_storage_quota_default": "Domyślna ilość miejsca w magazynie (GiB)", "oauth_storage_quota_default_description": "Limit w GiB do wykorzystania, gdy nie podano żadnej wartości.", "oauth_timeout": "Upłynął czas żądania", - "oauth_timeout_description": "Limit czasu żądania (w milisekundach)", + "oauth_timeout_description": "Limit czasu żądania w milisekundach", "password_enable_description": "Zaloguj używając e-mail i hasła", "password_settings": "Logowanie Hasłem", "password_settings_description": "Zarządzaj ustawieniami logowania hasłem", @@ -387,8 +395,6 @@ "admin_password": "Hasło Administratora", "administration": "Administracja", "advanced": "Zaawansowane", - "advanced_settings_beta_timeline_subtitle": "Wypróbuj nową funkcjonalność aplikacji", - "advanced_settings_beta_timeline_title": "Beta-Timeline", "advanced_settings_enable_alternate_media_filter_subtitle": "Użyj tej opcji do filtrowania mediów podczas synchronizacji alternatywnych kryteriów. Używaj tylko wtedy gdy aplikacja ma problemy z wykrywaniem wszystkich albumów.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERYMENTALNE] Użyj alternatywnego filtra synchronizacji albumu", "advanced_settings_log_level_title": "Poziom szczegółowości dziennika: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne", "advanced_settings_proxy_headers_subtitle": "Zdefiniuj nagłówki proxy, które Immich powinien wysyłać z każdym żądaniem sieciowym", "advanced_settings_proxy_headers_title": "Nagłówki proxy", + "advanced_settings_readonly_mode_subtitle": "Włącza tryb tylko do odczytu, w którym zdjęcia można tylko przeglądać, a takie czynności jak wybieranie wielu obrazów, udostępnianie, przesyłanie i usuwanie są wyłączone. Włącz/wyłącz tryb tylko do odczytu za pomocą awatara użytkownika na ekranie głównym", + "advanced_settings_readonly_mode_title": "Tryb tylko do odczytu", "advanced_settings_self_signed_ssl_subtitle": "Pomija weryfikację certyfikatu SSL dla punktu końcowego serwera. Wymagane w przypadku certyfikatów z podpisem własnym.", "advanced_settings_self_signed_ssl_title": "Zezwalaj na certyfikaty SSL z podpisem własnym", "advanced_settings_sync_remote_deletions_subtitle": "Automatycznie usuń lub przywróć zasób na tym urządzeniu po wykonaniu tej czynności w interfejsie webowym", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Na pewno chcesz usunąć {user}?", "album_search_not_found": "Nie znaleziono albumów pasujących do Twojego wyszukiwania", "album_share_no_users": "Wygląda na to, że ten album albo udostępniono wszystkim użytkownikom, albo nie ma komu go udostępnić.", + "album_summary": "Podsumowanie albumu", "album_updated": "Album zaktualizowany", "album_updated_setting_description": "Otrzymaj powiadomienie e-mail, gdy do udostępnionego Ci albumu zostaną dodane nowe zasoby", "album_user_left": "Opuszczono {album}", @@ -441,7 +450,7 @@ "albums_default_sort_order": "Domyślna kolejność sortowania w albumach", "albums_default_sort_order_description": "Początkowa kolejność sortowania zasobów przy tworzeniu nowych albumów.", "albums_feature_description": "Kolekcje zasobów, które można udostępniać innym użytkownikom.", - "albums_on_device_count": "Albumów na urzadzeniu ({count})", + "albums_on_device_count": "Albumy na urządzeniu ({count})", "all": "Wszystkie", "all_albums": "Wszystkie albumy", "all_people": "Wszystkie osoby", @@ -459,8 +468,9 @@ "app_bar_signout_dialog_content": "Czy na pewno chcesz się wylogować?", "app_bar_signout_dialog_ok": "Tak", "app_bar_signout_dialog_title": "Wyloguj się", - "app_settings": "Ustawienia Aplikacji", + "app_settings": "Ustawienia aplikacji", "appears_in": "W albumach", + "apply_count": "Zastosuj ({count, number})", "archive": "Archiwum", "archive_action_prompt": "{count} dodanych do Archiwum", "archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Zasób został pomyślnie przywrócony", "asset_skipped": "Pominięto", "asset_skipped_in_trash": "W koszu", + "asset_trashed": "Zasób wrzucono do kosza", + "asset_troubleshoot": "Rozwiązywanie problemów z zasobami", "asset_uploaded": "Przesłano", "asset_uploading": "Przesyłanie…", "asset_viewer_settings_subtitle": "Zarządzaj ustawieniami przeglądarki galerii", @@ -500,8 +512,8 @@ "assets": "Zasoby", "assets_added_count": "Dodano {count, plural, one {# zasób} few {# zasoby} other {# zasobów}}", "assets_added_to_album_count": "Dodano {count, plural, one {# zasób} few {# zasoby} other {# zasobów}} do albumu", - "assets_added_to_albums_count": "Dodano {assetTotal, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}} do {albumTotal} albumów", - "assets_cannot_be_added_to_album_count": "{count, plural, one {sztuka Elementu} other {szt. Elementów}} nie może być dodana do albumu", + "assets_added_to_albums_count": "Dodano {assetTotal, plural, one {# zasób} few {# zasoby} other {# zasobów}} do {albumTotal, plural, one {# albumu} other {# albumów}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Zasób nie może zostać dodany} other {Zasoby nie mogą zostać dodane}} do albumu", "assets_cannot_be_added_to_albums": "{count, plural, one {Zasób nie może być dodany} other {Zasoby nie mogą być dodane}} do żadnego z albumów", "assets_count": "{count, plural, one {# zasób} few {# zasoby} other {# zasobów}}", "assets_deleted_permanently": "{count} zostało trwale usuniętych", @@ -518,32 +530,35 @@ "assets_trashed": "{count} szt. zostało wrzucone do kosza", "assets_trashed_count": "Wrzucono do kosza {count, plural, one {# zasób} few {# zasoby} other {# zasobów}}", "assets_trashed_from_server": "{count} szt. usuniętych z serwera Immich", - "assets_were_part_of_album_count": "{count, plural, one {Zasób był} few {Zasoby były} many {Zasobów było} other {Zasobów było}} już częścią albumu", + "assets_were_part_of_album_count": "{count, plural, one {Zasób był} other {Zasoby były}} już częścią albumu", "assets_were_part_of_albums_count": "{count, plural, one {Zasób był} other {Zasoby były}} już częścią albumów", - "authorized_devices": "Upoważnione Urządzenia", + "authorized_devices": "Autoryzowane urządzenia", "automatic_endpoint_switching_subtitle": "Połącz się lokalnie przez wyznaczoną sieć Wi-Fi, jeśli jest dostępna, i korzystaj z alternatywnych połączeń gdzie indziej", "automatic_endpoint_switching_title": "Automatyczne przełączanie adresów URL", "autoplay_slideshow": "Automatyczne odtwarzanie pokazu slajdów", "back": "Wstecz", "back_close_deselect": "Wróć, zamknij lub odznacz", + "background_backup_running_error": "Tworzenie kopii zapasowej w tle jest obecnie w toku, nie można rozpocząć ręcznego tworzenia kopii zapasowej", "background_location_permission": "Uprawnienia do lokalizacji w tle", "background_location_permission_content": "Aby móc przełączać sieć podczas pracy w tle, Immich musi *zawsze* mieć dostęp do dokładnej lokalizacji, aby aplikacja mogła odczytać nazwę sieci Wi-Fi", - "backup": "Kopia Zapasowa", + "background_options": "Opcje w tle", + "backup": "Kopia zapasowa", "backup_album_selection_page_albums_device": "Albumy na urządzeniu ({count})", "backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć", "backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.", - "backup_album_selection_page_select_albums": "Zaznacz albumy", + "backup_album_selection_page_select_albums": "Wybierz albumy", "backup_album_selection_page_selection_info": "Info o wyborze", "backup_album_selection_page_total_assets": "Łącznie unikalnych plików", + "backup_albums_sync": "Synchronizacja kopii zapasowych albumów", "backup_all": "Wszystkie", "backup_background_service_backup_failed_message": "Nie udało się wykonać kopii zapasowej zasobów. Ponowna próba…", "backup_background_service_connection_failed_message": "Nie udało się połączyć z serwerem. Ponowna próba…", - "backup_background_service_current_upload_notification": "Wysyłanie {filename}", + "backup_background_service_current_upload_notification": "Przesyłanie {filename}", "backup_background_service_default_notification": "Sprawdzanie nowych zasobów…", "backup_background_service_error_title": "Błąd kopii zapasowej", "backup_background_service_in_progress_notification": "Tworzenie kopii zapasowej twoich zasobów…", "backup_background_service_upload_failure_notification": "Błąd przesyłania {filename}", - "backup_controller_page_albums": "Kopia Zapasowa albumów", + "backup_controller_page_albums": "Albumy z włączoną kopią zapasową", "backup_controller_page_background_app_refresh_disabled_content": "Włącz odświeżanie aplikacji w tle w Ustawienia > Ogólne > Odświeżanie aplikacji w tle, aby móc korzystać z kopii zapasowej w tle.", "backup_controller_page_background_app_refresh_disabled_title": "Odświeżanie aplikacji w tle wyłączone", "backup_controller_page_background_app_refresh_enable_button_text": "Przejdź do ustawień", @@ -560,29 +575,29 @@ "backup_controller_page_background_turn_off": "Wyłącz usługę w tle", "backup_controller_page_background_turn_on": "Włącz usługę w tle", "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": "Skopiowane zdjęcia oraz filmy", + "backup_controller_page_backup": "Kopia zapasowa", + "backup_controller_page_backup_selected": "Wybrane: ", + "backup_controller_page_backup_sub": "Zdjęcia i filmy z utworzoną kopią zapasową", "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_desc_backup": "Włącz kopię zapasową na pierwszym planie, aby automatycznie przesyłać nowe zasoby na serwer po otworzeniu aplikacji.", "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", - "backup_controller_page_remainder_sub": "Pozostałe zdjęcia i albumy do wykonania kopii zapasowej z wyboru", + "backup_controller_page_none_selected": "Nic nie wybrano", + "backup_controller_page_remainder": "Pozostałe", + "backup_controller_page_remainder_sub": "Pozostałe zdjęcia i filmy wybrane do wykonania kopii zapasowej", "backup_controller_page_server_storage": "Pamięć Serwera", "backup_controller_page_start_backup": "Rozpocznij Kopię Zapasową", - "backup_controller_page_status_off": "Kopia Zapasowa jest wyłaczona", - "backup_controller_page_status_on": "Kopia Zapasowa jest włączona", - "backup_controller_page_storage_format": "{used} z {total} wykorzystanych", - "backup_controller_page_to_backup": "Albumy z Kopią Zapasową", + "backup_controller_page_status_off": "Automatyczne tworzenie kopii zapasowej na pierwszym planie jest wyłączone", + "backup_controller_page_status_on": "Automatyczne tworzenie kopii zapasowej na pierwszym planie jest włączone", + "backup_controller_page_storage_format": "Wykorzystano {used} z {total}", + "backup_controller_page_to_backup": "Albumy, dla których ma być tworzona kopia zapasowa", "backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów", - "backup_controller_page_turn_off": "Wyłącz Kopię Zapasową", - "backup_controller_page_turn_on": "Włącz Kopię Zapasową", - "backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku", + "backup_controller_page_turn_off": "Wyłącz kopię zapasową na pierwszym planie", + "backup_controller_page_turn_on": "Włącz kopię zapasową na pierwszym planie", + "backup_controller_page_uploading_file_info": "Informacje o przesyłanym pliku", "backup_err_only_album": "Nie można usunąć jedynego albumu", "backup_info_card_assets": "zasoby", "backup_manual_cancelled": "Anulowano", @@ -590,12 +605,10 @@ "backup_manual_success": "Sukces", "backup_manual_title": "Stan przesyłania", "backup_options": "Opcje kopii zapasowej", - "backup_options_page_title": "Opcje kopi zapasowej", + "backup_options_page_title": "Opcje kopii zapasowej", "backup_setting_subtitle": "Zarządzaj ustawieniami przesyłania w tle i na pierwszym planie", - "backup_settings_subtitle": "Zarządzanie ustawieniami wysyłania", + "backup_settings_subtitle": "Zarządzanie ustawieniami przesyłania", "backward": "Do tyłu", - "beta_sync": "Status synchronizacji w wersji Beta", - "beta_sync_subtitle": "Zarządzaj nowym systemem synchronizacji", "biometric_auth_enabled": "Włączono logowanie biometryczne", "biometric_locked_out": "Uwierzytelnianie biometryczne jest dla Ciebie zablokowane", "biometric_no_options": "Brak możliwości biometrii", @@ -637,7 +650,7 @@ "cast": "Odtwórz na telewizorze", "cast_description": "Skonfiguruj dostępne cele do przesyłania", "change_date": "Zmień datę", - "change_description": "Zmiana opisu", + "change_description": "Zmień opis", "change_display_order": "Zmień kolejność wyświetlania", "change_expiration_time": "Zmień czas ważności", "change_location": "Zmień lokalizację", @@ -653,6 +666,8 @@ "change_pin_code": "Zmień kod PIN", "change_your_password": "Zmień swoje hasło", "changed_visibility_successfully": "Pomyślnie zmieniono widoczność", + "charging": "Ładowanie", + "charging_requirement_mobile_backup": "Tworzenie kopii zapasowej w tle wymaga by urządzenie było podłączone do ładowania", "check_corrupt_asset_backup": "Sprawdź, czy kopie zapasowe zasobów nie są uszkodzone", "check_corrupt_asset_backup_button": "Wykonaj sprawdzenie", "check_corrupt_asset_backup_description": "Uruchom sprawdzenie tylko przez Wi-Fi i po utworzeniu kopii zapasowej wszystkich zasobów. Procedura może potrwać kilka minut.", @@ -690,7 +705,7 @@ "confirm_admin_password": "Potwierdź Hasło Administratora", "confirm_delete_face": "Czy na pewno chcesz usunąć twarz {name} z zasobów?", "confirm_delete_shared_link": "Czy na pewno chcesz usunąć ten udostępniony link?", - "confirm_keep_this_delete_others": "Wszystkie inne zasoby zostaną usunięte poza tym zasobem. Czy jesteś pewien, że chcesz kontynuować?", + "confirm_keep_this_delete_others": "Wszystkie inne zasoby w tym stosie, z wyjątkiem tego zasobu, zostaną usunięte. Czy jesteś pewien, że chcesz kontynuować?", "confirm_new_pin_code": "Potwierdź nowy kod PIN", "confirm_password": "Potwierdź hasło", "confirm_tag_face": "Chcesz dodać do tej twarzy etykietę {name}?", @@ -707,7 +722,7 @@ "control_bottom_app_bar_edit_time": "Edytuj datę i godzinę", "control_bottom_app_bar_share_link": "Udostępnij link", "control_bottom_app_bar_share_to": "Wyślij", - "control_bottom_app_bar_trash_from_immich": "Przenieść do kosza", + "control_bottom_app_bar_trash_from_immich": "Przenieś do kosza", "copied_image_to_clipboard": "Skopiowano obraz do schowka.", "copied_to_clipboard": "Skopiowano do schowka!", "copy_error": "Błąd kopiowania", @@ -739,6 +754,7 @@ "create_user": "Stwórz użytkownika", "created": "Utworzono", "created_at": "Utworzony", + "creating_linked_albums": "Tworzenie połączonych albumów...", "crop": "Przytnij", "curated_object_page_title": "Rzeczy", "current_device": "Obecne urządzenie", @@ -773,7 +789,7 @@ "delete_api_key_prompt": "Czy na pewno chcesz usunąć ten klucz API?", "delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia", "delete_dialog_alert_local": "Elementy te zostaną trwale usunięte z Twojego urządzenia, ale nadal będą dostępne na serwerze Immich", - "delete_dialog_alert_local_non_backed_up": "Kopia zapasowa niektórych elementów nie jest tworzona w Immich i zostanie trwale usunięta z Twojego urządzenia", + "delete_dialog_alert_local_non_backed_up": "Niektóre elementy nie mają kopii zapasowej w Immich i zostaną trwale usunięte z Twojego urządzenia", "delete_dialog_alert_remote": "Elementy te zostaną trwale usunięte z serwera Immich", "delete_dialog_ok_force": "Usuń mimo to", "delete_dialog_title": "Usuń trwale", @@ -785,7 +801,7 @@ "delete_local_action_prompt": "{count} lokalnie usunięto", "delete_local_dialog_ok_backed_up_only": "Usuń tylko kopię zapasową", "delete_local_dialog_ok_force": "Usuń mimo to", - "delete_others": "Usuń inne", + "delete_others": "Usuń pozostałe", "delete_permanently": "Usuń trwale", "delete_permanently_action_prompt": "{count} trwale usuniętych", "delete_shared_link": "Usuń udostępniony link", @@ -836,7 +852,7 @@ "downloading": "Pobieranie", "downloading_asset_filename": "Pobieranie zasobu {filename}", "downloading_media": "Pobieranie multimediów", - "drop_files_to_upload": "Upuść pliki gdziekolwiek, aby je załadować", + "drop_files_to_upload": "Upuść pliki w dowolnym miejscu, aby je przesłać", "duplicates": "Duplikaty", "duplicates_description": "Rozstrzygnij każdą grupę, określając, które zasoby są duplikatami, jeżeli są duplikatami", "duration": "Czas trwania", @@ -887,8 +903,10 @@ "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", + "error_delete_face": "Błąd podczas usuwania twarzy z zasobów", + "error_getting_places": "Błąd podczas pozyskiwania lokalizacji", "error_loading_image": "Błąd podczas ładowania zdjęcia", + "error_loading_partners": "Błąd podczas ładowania partnerów: {error}", "error_saving_image": "Błąd: {error}", "error_tag_face_bounding_box": "Błąd przy dodawaniu etykiety dla tej twarzy - nie może uzyskać współrzędnych granicznych", "error_title": "Błąd - Coś poszło nie tak", @@ -897,7 +915,7 @@ "cannot_navigate_previous_asset": "Nie można przejść do poprzedniego zasobu", "cant_apply_changes": "Nie można zastosować zmian", "cant_change_activity": "Nie można {enabled, select, true {wyłączyć} other {włączyć}} aktywności", - "cant_change_asset_favorite": "Nie można zmienić ulubionego dla zasobu", + "cant_change_asset_favorite": "Nie można zmienić statusu ulubionego dla zasobu", "cant_change_metadata_assets_count": "Nie można zmienić metadanych {count, plural, one {# zasobu} other {# zasobów}}", "cant_get_faces": "Nie można pozyskać twarzy", "cant_get_number_of_comments": "Nie można uzyskać liczby komentarzy", @@ -922,7 +940,7 @@ "failed_to_load_people": "Nie udało się pobrać ludzi", "failed_to_remove_product_key": "Nie udało się usunąć klucza produktu", "failed_to_reset_pin_code": "Nie udało się zresetować kodu PIN", - "failed_to_stack_assets": "Nie udało się zestawić zasobów", + "failed_to_stack_assets": "Nie udało się utworzyć stosu z zasobów", "failed_to_unstack_assets": "Nie udało się rozdzielić zasobów", "failed_to_update_notification_status": "Nie udało się zaktualizować stanu powiadomienia", "import_path_already_exists": "Ta ścieżka importu już istnieje.", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "Nie znaleziono ulubionych zasobów", "feature_photo_updated": "Zdjęcie główne zaktualizowane pomyślnie", "features": "Funkcje", + "features_in_development": "Funkcje w fazie rozwoju", "features_setting_description": "Zarządzaj funkcjami aplikacji", "file_name": "Nazwa pliku", "file_name_or_extension": "Nazwie lub rozszerzeniu pliku", @@ -1071,14 +1090,17 @@ "forgot_pin_code_question": "Nie pamiętasz kodu PIN?", "forward": "Do przodu", "gcast_enabled": "Google Cast", - "gcast_enabled_description": "Ta funkcja pobiera zewnętrzne zasoby z Google, aby działać.", + "gcast_enabled_description": "Ta funkcja , aby działać, ładuje zewnętrzne zasoby z Google.", "general": "Ogólne", + "geolocation_instruction_location": "Kliknij na zasób z współrzędnymi GPS, aby użyć jego lokalizacji, lub wybierz lokalizację bezpośrednio z mapy", "get_help": "Pomoc", "get_wifiname_error": "Nie można uzyskać nazwy Wi-Fi. Upewnij się, że udzieliłeś niezbędnych uprawnień i jesteś połączony z siecią Wi-Fi", "getting_started": "Pierwsze kroki", "go_back": "Wstecz", "go_to_folder": "Idź do folderu", "go_to_search": "Przejdź do wyszukiwania", + "gps": "GPS", + "gps_missing": "Brak GPS", "grant_permission": "Udziel pozwolenia", "group_albums_by": "Grupuj albumy...", "group_country": "Grupuj według państwa", @@ -1147,7 +1169,7 @@ "immich_web_interface": "Interfejs internetowy Immich", "import_from_json": "Wczytaj z JSON", "import_path": "Ścieżka importu", - "in_albums": "W {count, plural, one {# album} other {# albumy}}", + "in_albums": "W {count, plural, one {# albumie} other {# albumach}}", "in_archive": "W archiwum", "include_archived": "Uwzględnij zarchiwizowane", "include_shared_albums": "Uwzględnij udostępnione albumy", @@ -1171,11 +1193,11 @@ "ios_debug_info_no_sync_yet": "Nie uruchomiono jeszcze żadnego zadania synchronizacji w tle", "ios_debug_info_processes_queued": "{count, plural, one {{count} proces w tle w kolejce} few {{count} procesy w tle w kolejce} other {{count} procesów w tle w kolejce}}", "ios_debug_info_processing_ran_at": "Przetwarzanie przebiegło {dateTime}", - "items_count": "{count, plural, one {# element} other {# elementy}}", + "items_count": "{count, plural, one {# element} few {# elementy} other {# elementów}}", "jobs": "Zadania", "keep": "Zachowaj", "keep_all": "Zachowaj wszystko", - "keep_this_delete_others": "Zachowaj to, usuń inne", + "keep_this_delete_others": "Zachowaj to, usuń pozostałe", "kept_this_deleted_others": "Zachowano ten zasób i usunięto {count, plural, one {#zasób} other {#zasoby}}", "keyboard_shortcuts": "Skróty klawiaturowe", "language": "Język", @@ -1186,7 +1208,7 @@ "large_files": "Duże pliki", "last": "Ostatni", "last_seen": "Ostatnio widziane", - "latest_version": "Najnowsza Wersja", + "latest_version": "Najnowsza wersja", "latitude": "Szerokość geograficzna", "leave": "Opuść", "leave_album": "Opuść album", @@ -1214,6 +1236,7 @@ "local": "Lokalny", "local_asset_cast_failed": "Nie można strumieniować zasobu, który nie został przesłany na serwer", "local_assets": "Zasoby lokalne", + "local_media_summary": "Podsumowanie lokalnych mediów", "local_network": "Sieć lokalna", "local_network_sheet_info": "Aplikacja połączy się z serwerem za pośrednictwem tego adresu URL podczas korzystania z określonej sieci Wi-Fi", "location_permission": "Zezwolenie na lokalizację", @@ -1225,6 +1248,7 @@ "location_picker_longitude_hint": "Wpisz tutaj swoją długość geograficzną", "lock": "Zablokuj", "locked_folder": "Folder zablokowany", + "log_detail_title": "Szczegóły dziennika", "log_out": "Wyloguj", "log_out_all_devices": "Wyloguj ze Wszystkich Urządzeń", "logged_in_as": "Zalogowano jako {user}", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "Hasło zostało zmienione", "logout_all_device_confirmation": "Czy na pewno chcesz wylogować się ze wszystkich urządzeń?", "logout_this_device_confirmation": "Czy na pewno chcesz wylogować to urządzenie?", + "logs": "Logi", "longitude": "Długość geograficzna", "look": "Wygląd", "loop_videos": "Powtarzaj filmy", @@ -1262,6 +1287,7 @@ "main_branch_warning": "Używasz wersji deweloperskiej. Zdecydowanie zalecamy korzystanie z wydanej wersji aplikacji!", "main_menu": "Menu główne", "make": "Marka", + "manage_geolocation": "Zarządzaj lokalizacją", "manage_shared_links": "Zarządzaj udostępnionymi linkami", "manage_sharing_with_partners": "Zarządzaj dzieleniem z partnerami", "manage_the_app_settings": "Zarządzaj ustawieniami aplikacji", @@ -1296,6 +1322,7 @@ "mark_as_read": "Zaznacz jako odczytane", "marked_all_as_read": "Zaznaczono wszystkie jako przeczytane", "matches": "Powiązania", + "matching_assets": "Pasujące zasoby", "media_type": "Typ zasobu", "memories": "Wspomnienia", "memories_all_caught_up": "Wszystko złapane", @@ -1336,6 +1363,7 @@ "name_or_nickname": "Nazwa lub pseudonim", "network_requirement_photos_upload": "Używaj danych komórkowych do tworzenia kopii zapasowych zdjęć", "network_requirement_videos_upload": "Używaj danych komórkowych do tworzenia kopii zapasowych filmów", + "network_requirements": "Wymagania sieciowe", "network_requirements_updated": "Zmieniono wymagania sieciowe, resetowanie kolejki kopii zapasowych", "networking_settings": "Sieć", "networking_subtitle": "Zarządzaj ustawieniami punktu końcowego serwera", @@ -1346,6 +1374,7 @@ "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_timeline": "Nowa oś czasu", "new_user_created": "Pomyślnie stworzono nowego użytkownika", "new_version_available": "NOWA WERSJA DOSTĘPNA", "newest_first": "Od najnowszych", @@ -1359,20 +1388,25 @@ "no_assets_message": "KLIKNIJ, ABY WYSŁAĆ PIERWSZE ZDJĘCIE", "no_assets_to_show": "Brak zasobów do pokazania", "no_cast_devices_found": "Nie znaleziono urządzeń do przesyłania strumieniowego", + "no_checksum_local": "Brak sumy kontrolnej - nie można pobrać lokalnych zasobów", + "no_checksum_remote": "Brak sumy kontrolnej - nie można pobrać zdalnego zasobu", "no_duplicates_found": "Nie znaleziono duplikatów.", "no_exif_info_available": "Nie znaleziono informacji exif", "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_local_assets_found": "Nie znaleziono żadnych lokalnych zasobów o tej sumie kontrolnej", "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", "no_places": "Brak miejsc", + "no_remote_assets_found": "Nie znaleziono żadnych zdalnych zasobów o tej sumie kontrolnej", "no_results": "Brak wyników", "no_results_description": "Spróbuj użyć synonimu lub bardziej ogólnego słowa kluczowego", "no_shared_albums_message": "Stwórz album aby udostępnić zdjęcia i filmy osobom w Twojej sieci", "no_uploads_in_progress": "Brak przesyłań w toku", + "not_available": "Nie dotyczy", "not_in_any_album": "Bez albumu", "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", @@ -1394,7 +1428,7 @@ "on_this_device": "Na tym urządzeniu", "onboarding": "Wdrożenie", "onboarding_locale_description": "Wybierz preferowany język. Można to później zmienić w ustawieniach.", - "onboarding_privacy_description": "Śledzenie (opcjonalne) funkcja opiera się na zewnętrznych usługach i może zostać wyłączona w dowolnym momencie w ustawieniach.", + "onboarding_privacy_description": "Następujące (opcjonalne) funkcje opierają się na usługach zewnętrznych i można je w dowolnym momencie wyłączyć w ustawieniach.", "onboarding_server_welcome_description": "Skonfigurujmy twoją instancję z kilkoma typowymi ustawieniami.", "onboarding_theme_description": "Wybierz motyw kolorystyczny dla twojej instancji. Możesz go później zmienić w ustawieniach.", "onboarding_user_welcome_description": "Zaczynamy!", @@ -1407,6 +1441,8 @@ "open_the_search_filters": "Otwórz filtry wyszukiwania", "options": "Opcje", "or": "lub", + "organize_into_albums": "Uporządkuj w albumy", + "organize_into_albums_description": "Umieść istniejące zdjęcia w albumach przy użyciu bieżących ustawień synchronizacji", "organize_your_library": "Organizuj swoją bibliotekę", "original": "oryginalny", "other": "Inne", @@ -1417,17 +1453,17 @@ "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_assets": "Wszystkie Twoje 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", - "partner_list_user_photos": "{user} zdjęcia", + "partner_list_user_photos": "Zdjęcia należące do {user}", "partner_list_view_all": "Pokaż wszystkie", "partner_page_empty_message": "Twoje zdjęcia nie są udostępnione żadnemu partnerowi.", "partner_page_no_more_users": "Brak użytkowników do dodania", "partner_page_partner_add_failed": "Nie udało się dodać partnera", "partner_page_select_partner": "Wybierz partnera", "partner_page_shared_to_title": "Udostępniono", - "partner_page_stop_sharing_content": "{partner} nie będzie już mieć dostępu do twoich zdjęć.", - "partner_sharing": "Dzielenie z Partnerami", + "partner_page_stop_sharing_content": "{partner} nie będzie już mieć dostępu do Twoich zdjęć.", + "partner_sharing": "Dzielenie z partnerami", "partners": "Partnerzy", "password": "Hasło", "password_does_not_match": "Hasła nie są takie same", @@ -1452,7 +1488,7 @@ "permanent_deletion_warning_setting_description": "Pokaż ostrzeżenie przy trwałym usuwaniu zasobów", "permanently_delete": "Usuń trwale", "permanently_delete_assets_count": "Trwale usuń {count, plural, one {zasób} few {zasoby} many {zasobów} other {zasobów}}", - "permanently_delete_assets_prompt": "Czy na pewno chcesz trwale usunąć {count, plural, one {ten zasób?} other {te # zasoby?}} Spowoduje to również usunięcie {count, plural, one {go z jego} other {ich z ich}} album(ów).", + "permanently_delete_assets_prompt": "Czy na pewno chcesz trwale usunąć {count, plural, one {ten zasób?} few {te # zasoby?} other {te # zasobów?}} Spowoduje to również usunięcie {count, plural, one {go z jego} other {ich z ich}} album(ów).", "permanently_deleted_asset": "Pomyślnie trwale usunięto zasób", "permanently_deleted_assets_count": "Trwale usunięto {count, plural, one {# zasób} other {# zasobów}}", "permission": "Pozwolenie", @@ -1492,6 +1528,7 @@ "port": "Port", "preferences_settings_subtitle": "Zarządzaj preferencjami aplikacji", "preferences_settings_title": "Ustawienia", + "preparing": "Przygotowywanie", "preset": "Ustawienie", "preview": "Podgląd", "previous": "Poprzedni", @@ -1504,12 +1541,13 @@ "privacy": "Prywatność", "profile": "Profil", "profile_drawer_app_logs": "Logi", - "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_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej głównej wersji.", + "profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej pomniejszej wersji.", "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_drawer_readonly_mode": "Włączono tryb tylko do odczytu. Aby wyjść, naciśnij i przytrzymaj ikonę awatara użytkownika.", + "profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej głównej wersji.", + "profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej pomniejszej wersji.", "profile_image_of_user": "Zdjęcie profilowe {user}", "profile_picture_set": "Zdjęcie profilowe ustawione.", "public_album": "Publiczny album", @@ -1546,6 +1584,7 @@ "purchase_server_description_2": "Status wspierającego", "purchase_server_title": "Serwer", "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora", + "query_asset_id": "Zapytanie o ID zasobu", "queue_status": "Kolejkowanie {count}/{total}", "rating": "Ocena gwiazdkowa", "rating_clear": "Wyczyść ocenę", @@ -1553,6 +1592,9 @@ "rating_description": "Wyświetl ocenę z EXIF w panelu informacji", "reaction_options": "Opcje reakcji", "read_changelog": "Zobacz Zmiany", + "readonly_mode_disabled": "Tryb tylko do odczytu wyłączony", + "readonly_mode_enabled": "Tryb tylko do odczytu włączony", + "ready_for_upload": "Gotowe do przesłania", "reassign": "Przypisz ponownie", "reassigned_assets_to_existing_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do {name, select, null {istniejącej osoby} other {{name}}}", "reassigned_assets_to_new_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do nowej osoby", @@ -1577,8 +1619,9 @@ "regenerating_thumbnails": "Regenerowanie miniatur", "remote": "Zdalny", "remote_assets": "Zasoby zdalne", + "remote_media_summary": "Podsumowanie mediów zdalnych", "remove": "Usuń", - "remove_assets_album_confirmation": "Czy na pewno chcesz usunąć {count, plural, one {# zasób} other {# zasoby}} z albumu?", + "remove_assets_album_confirmation": "Czy na pewno chcesz usunąć {count, plural, one {# zasób} few {# zasoby} other {# zasobów}} z albumu?", "remove_assets_shared_link_confirmation": "Czy na pewno chcesz usunąć {count, plural, one {# zasób} other {# zasoby}} z tego udostępnionego linku?", "remove_assets_title": "Usunąć zasoby?", "remove_custom_date_range": "Usuń niestandardowy zakres dat", @@ -1590,8 +1633,8 @@ "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", + "remove_memory": "Usuń wspomnienie", + "remove_photo_from_memory": "Usuń zdjęcia z tych wspomnień", "remove_tag": "Usuń tag", "remove_url": "Usuń URL", "remove_user": "Usuń użytkownika", @@ -1599,15 +1642,15 @@ "removed_from_archive": "Usunięto z archiwum", "removed_from_favorites": "Usunięto z ulubionych", "removed_from_favorites_count": "{count, plural, other {Usunięto #}} z ulubionych", - "removed_memory": "Pamięć została usunięta", - "removed_photo_from_memory": "Usunięto zdjęcie z pamięci", + "removed_memory": "Wspomnienie usunięte", + "removed_photo_from_memory": "Usunięto zdjęcie ze wspomnień", "removed_tagged_assets": "Usunięto etykietę z {count, plural, one {# zasobu} other {# zasobów}}", "rename": "Zmień nazwę", "repair": "Napraw", "repair_no_results_message": "Tutaj pojawią się nieśledzone i brakujące pliki", "replace_with_upload": "Prześlij nową wersję", "repository": "Repozytorium", - "require_password": "Wymagaj hasło", + "require_password": "Wymagaj hasła", "require_user_to_change_password_on_first_login": "Zmuś użytkownika do zmiany hasła podczas następnego logowania", "rescan": "Ponowne skanowanie", "reset": "Reset", @@ -1629,6 +1672,7 @@ "restore_user": "Przywróć użytkownika", "restored_asset": "Przywrócony zasób", "resume": "Wznów", + "resume_paused_jobs": "Wznów {count, plural, one {# wstrzymane zadanie} few {# wstrzymane zadania} other {# wstrzymanych zadań}}", "retry_upload": "Prześlij ponownie", "review_duplicates": "Przejrzyj duplikaty", "review_large_files": "Przejrzyj duże pliki", @@ -1651,7 +1695,7 @@ "search_albums": "Przeszukaj albumy", "search_by_context": "Wyszukaj według treści", "search_by_description": "Wyszukaj według opisu", - "search_by_description_example": "Jednodniowa wycieczka górska w Bieszczady", + "search_by_description_example": "Całodniowa wycieczka w Bieszczady", "search_by_filename": "Szukaj według nazwy pliku lub rozszerzenia", "search_by_filename_example": "np. IMG_1234.JPG lub PNG", "search_camera_make": "Wyszukaj markę aparatu...", @@ -1700,7 +1744,7 @@ "search_tags": "Wyszukaj etykiety...", "search_timezone": "Wyszukaj strefę czasową...", "search_type": "Wyszukaj w", - "search_your_photos": "Szukaj swoich zdjęć", + "search_your_photos": "Przeszukaj swoje zdjęcia", "searching_locales": "Wyszukaj region...", "second": "Sekunda", "see_all_people": "Zobacz wszystkie osoby", @@ -1720,12 +1764,13 @@ "select_photos": "Wybierz zdjęcia", "select_trash_all": "Zaznacz wszystko do kosza", "select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu", - "selected": "Zaznaczone", + "selected": "Wybrane", "selected_count": "{count, plural, other {# wybrane}}", + "selected_gps_coordinates": "Wybrane Współrzędne GPS", "send_message": "Wyślij wiadomość", "send_welcome_email": "Wyślij e-mail powitalny", "server_endpoint": "Punkt końcowy serwera", - "server_info_box_app_version": "Wersja Aplikacji", + "server_info_box_app_version": "Wersja aplikacji", "server_info_box_server_url": "Adres URL", "server_offline": "Serwer Offline", "server_online": "Serwer Online", @@ -1850,6 +1895,7 @@ "show_slideshow_transition": "Pokaż przejście pokazu slajdów", "show_supporter_badge": "Odznaka wspierającego", "show_supporter_badge_description": "Pokaż odznakę wspierającego", + "show_text_search_menu": "Pokaż menu wyszukiwania tekstowego", "shuffle": "Losuj", "sidebar": "Panel boczny", "sidebar_display_description": "Wyświetl link do widoku w pasku bocznym", @@ -1874,23 +1920,24 @@ "stack": "Stos", "stack_action_prompt": "{count} zgrupowano", "stack_duplicates": "Stos duplikatów", - "stack_select_one_photo": "Wybierz jedno główne zdjęcie do stosu", - "stack_selected_photos": "Układaj wybrane zdjęcia", - "stacked_assets_count": "Ułożone {count, plural, one {# zasób} other{# zasoby}}", + "stack_select_one_photo": "Wybierz jedno główne zdjęcie dla stosu", + "stack_selected_photos": "Utwórz stos z wybranych zdjęć", + "stacked_assets_count": "Utworzono stos z {count, plural, one {# zasobu} other {# zasobów}}", "stacktrace": "Ślad stosu", "start": "Start", "start_date": "Od dnia", + "start_date_before_end_date": "Data początkowa musi być wcześniejsza niż data końcowa", "state": "Województwo", "status": "Status", "stop_casting": "Zatrzymaj strumieniowanie", "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ęć.", + "stop_photo_sharing_description": "{partner} nie będzie już mieć dostępu do Twoich zdjęć.", "stop_sharing_photos_with_user": "Przestań udostępniać zdjęcia temu użytkownikowi", "storage": "Przestrzeń dyskowa", "storage_label": "Etykieta magazynu", "storage_quota": "Limit pamięci", - "storage_usage": "{used} z {available} użyte", + "storage_usage": "Wykorzystano {used} z {available}", "submit": "Zatwierdź", "success": "Sukces", "suggestions": "Sugestie", @@ -1901,9 +1948,11 @@ "swap_merge_direction": "Zmień kierunek złączenia", "sync": "Synchronizuj", "sync_albums": "Synchronizuj albumy", - "sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami kopii zapasowych", + "sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami z włączoną kopią zapasową", "sync_local": "Synchronizacja lokalna", "sync_remote": "Synchronizacja zdalna", + "sync_status": "Stan synchronizacji", + "sync_status_subtitle": "Wyświetl i zarządzaj systemem synchronizacji", "sync_upload_album_setting_subtitle": "Twórz i przesyłaj swoje zdjęcia i filmy do wybranych albumów w Immich", "tag": "Etykieta", "tag_assets": "Ustaw etykiety zasobów", @@ -1937,19 +1986,21 @@ "time_based_memories": "Wspomnienia oparte na czasie", "timeline": "Oś czasu", "timezone": "Strefa czasowa", - "to_archive": "Archiwum", + "to_archive": "Zarchiwizuj", "to_change_password": "Zmień hasło", "to_favorite": "Dodaj do ulubionych", "to_login": "Zaloguj się", + "to_multi_select": "aby wybrać wiele", "to_parent": "Idź do rodzica", + "to_select": "aby wybrać", "to_trash": "Kosz", "toggle_settings": "Przełącz ustawienia", - "total": "Całkowity", + "total": "Razem", "total_usage": "Całkowite wykorzystanie", "trash": "Kosz", "trash_action_prompt": "{count} przeniesione do kosza", "trash_all": "Usuń wszystkie", - "trash_count": "Kosz {count, number}", + "trash_count": "Usuń {count, number}", "trash_delete_asset": "Kosz/Usuń zasób", "trash_emptied": "Opróżnione śmieci", "trash_no_results_message": "Tu znajdziesz wyrzucone zdjęcia i filmy.", @@ -1961,10 +2012,11 @@ "trash_page_select_assets_btn": "Wybierz zasoby", "trash_page_title": "Kosz ({count})", "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {# dniach}}.", + "troubleshoot": "Rozwiąż problemy", "type": "Typ", "unable_to_change_pin_code": "Nie można zmienić kodu PIN", "unable_to_setup_pin_code": "Nie można ustawić kodu PIN", - "unarchive": "Cofnij archiwizację", + "unarchive": "Przywróć z archiwum", "unarchive_action_prompt": "{count} usunięto z archiwum", "unarchived_count": "{count, plural, one {# cofnięta archiwizacja} few {# cofnięte archiwizacje} other {# cofniętych archiwizacji}}", "undo": "Cofnij", @@ -1986,11 +2038,12 @@ "unselect_all": "Odznacz wszystko", "unselect_all_duplicates": "Odznacz wszystkie duplikaty", "unselect_all_in": "Odznacz wszystkie w {group}", - "unstack": "Rozłóż stos", - "unstack_action_prompt": "{count} odgrupowano", - "unstacked_assets_count": "{count, plural, one {Rozłożony # zasób} few {Rozłożone # zasoby} other {Rozłożonych # zasobów}}", + "unstack": "Rozdziel stos", + "unstack_action_prompt": "{count} rozdzielono", + "unstacked_assets_count": "Rozdzielono {count, plural, one {# zasób} few {# zasoby} other {# zasobów}}", "untagged": "Nieoznaczone", "up_next": "Do następnego", + "update_location_action_prompt": "Zaktualizuj lokalizację {count} wybranych zasobów na:", "updated_at": "Zaktualizowany", "updated_password": "Pomyślnie zaktualizowano hasło", "upload": "Prześlij", @@ -2057,11 +2110,12 @@ "view_next_asset": "Wyświetl następny zasób", "view_previous_asset": "Wyświetl poprzedni zasób", "view_qr_code": "Pokaż kod QR", - "view_stack": "Zobacz Ułożenie", + "view_similar_photos": "Zobacz podobne zdjęcia", + "view_stack": "Zobacz stos", "view_user": "Wyświetl użytkownika", "viewer_remove_from_stack": "Usuń ze stosu", "viewer_stack_use_as_main_asset": "Użyj jako głównego zasobu", - "viewer_unstack": "Rozłóż Stos", + "viewer_unstack": "Rozdziel stos", "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoby} other {# osób}}", "waiting": "Oczekujące", "warning": "Ostrzeżenie", @@ -2075,5 +2129,6 @@ "yes": "Tak", "you_dont_have_any_shared_links": "Nie masz żadnych udostępnionych linków", "your_wifi_name": "Twoja nazwa Wi-Fi", - "zoom_image": "Powiększ obraz" + "zoom_image": "Powiększ obraz", + "zoom_to_bounds": "Powiększ do krawędzi" } diff --git a/i18n/pt.json b/i18n/pt.json index 6ede1a04c4..b05c9288a8 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -28,6 +28,10 @@ "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_album_bottom_sheet_some_local_assets": "Alguns conteúdos locais não puderam ser adicionados no álbum", + "add_to_album_toggle": "Alternar seleção para {album}", + "add_to_albums": "Adicionar aos álbuns", + "add_to_albums_count": "Adicionar aos álbuns ({count})", "add_to_shared_album": "Adicionar ao álbum partilhado", "add_url": "Adicionar URL", "added_to_archive": "Adicionado ao arquivo", @@ -120,6 +124,13 @@ "logging_enable_description": "Ativar registo", "logging_level_description": "Quando ativado, qual o nível de log a usar.", "logging_settings": "Registo", + "machine_learning_availability_checks": "Verificação de disponibilidade", + "machine_learning_availability_checks_description": "Detectar automaticamente e dar preferência aos servidores de aprendizagem automática disponíveis", + "machine_learning_availability_checks_enabled": "Ativar confirmações de disponibilidade", + "machine_learning_availability_checks_interval": "Confirmação de intervalo", + "machine_learning_availability_checks_interval_description": "Intervalo, em milisegundos, entre confirmações de disponibilidade", + "machine_learning_availability_checks_timeout": "Tempo limite para requisição", + "machine_learning_availability_checks_timeout_description": "Tempo limite em milissegundos para verificações de disponibilidade", "machine_learning_clip_model": "Modelo CLIP", "machine_learning_clip_model_description": "O nome do modelo CLIP definido aqui. Tome nota de que é necessário voltar a executar a tarefa de \"Pesquisa Inteligente\" para todas as imagens depois de alterar o modelo.", "machine_learning_duplicate_detection": "Deteção de Itens Duplicados", @@ -355,6 +366,9 @@ "trash_number_of_days_description": "Número de dias para manter os ficheiros na reciclagem antes de os eliminar permanentemente", "trash_settings": "Definições da Reciclagem", "trash_settings_description": "Gerir definições da reciclagem", + "unlink_all_oauth_accounts": "Desvincular todas as contas OAuth", + "unlink_all_oauth_accounts_description": "Lembre-se de desvincular todas as contas OAuth antes de migrar para um novo provedor.", + "unlink_all_oauth_accounts_prompt": "Tem a certeza de que deseja desvincular todas as contas OAuth? Isso redefinirá o ID OAuth de cada utilizador e não poderá ser desfeito.", "user_cleanup_job": "Limpeza de utilizadores", "user_delete_delay": "A conta e os ficheiros de {user} serão agendados para eliminação permanente dentro de {delay, plural, one {# dia} other {# dias}}.", "user_delete_delay_settings": "Atraso de eliminação", @@ -381,8 +395,6 @@ "admin_password": "Palavra-passe do administrador", "administration": "Administração", "advanced": "Avançado", - "advanced_settings_beta_timeline_subtitle": "Experimente as novas funcionalidades da aplicação", - "advanced_settings_beta_timeline_title": "Linha temporal da versão Beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilize esta definição para filtrar ficheiros durante a sincronização baseada em critérios alternativos. Utilize apenas se a aplicação estiver com problemas a detetar todos os álbuns.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar um filtro alternativo de sincronização de álbuns em dispositivos", "advanced_settings_log_level_title": "Nível de registo: {level}", @@ -390,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Preferir imagens do servidor", "advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede", "advanced_settings_proxy_headers_title": "Cabeçalhos do Proxy", + "advanced_settings_readonly_mode_subtitle": "Ativa o modo somente leitura, onde as fotos podem ser visualizadas. Recursos como selecionar várias imagens, partilhar, transmitir e excluir ficam deactivados. Ativar/Desativar o modo somente leitura via avatar do utilizador na janela principal", + "advanced_settings_readonly_mode_title": "Modo somente leitura", "advanced_settings_self_signed_ssl_subtitle": "Não validar o certificado SSL com o endereço do servidor. Isto é necessário para certificados auto-assinados.", "advanced_settings_self_signed_ssl_title": "Permitir certificados SSL auto-assinados", "advanced_settings_sync_remote_deletions_subtitle": "Automaticamente eliminar ou restaurar um ficheiro neste dispositivo quando essa mesma ação for efetuada na web", @@ -417,6 +431,7 @@ "album_remove_user_confirmation": "Tem a certeza de que quer remover {user}?", "album_search_not_found": "Nenhum álbum encontrado segundo a pesquisa", "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores com quem o partilhar.", + "album_summary": "Resumo do álbum", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receber uma notificação por e-mail quando um álbum partilhado tiver novos ficheiros", "album_user_left": "Saíu do {album}", @@ -455,6 +470,7 @@ "app_bar_signout_dialog_title": "Sair", "app_settings": "Definições da Aplicação", "appears_in": "Aparece em", + "apply_count": "Aplicar ({count, number})", "archive": "Arquivo", "archive_action_prompt": "{count} adicionados ao Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", @@ -487,6 +503,8 @@ "asset_restored_successfully": "Arquivo restaurado com sucesso", "asset_skipped": "Ignorado", "asset_skipped_in_trash": "Na reciclagem", + "asset_trashed": "Ficheiro apagado", + "asset_troubleshoot": "Resolução de problemas com conteúdos", "asset_uploaded": "Enviado", "asset_uploading": "A enviar…", "asset_viewer_settings_subtitle": "Gerenciar as configurações do visualizador da galeria", @@ -494,7 +512,9 @@ "assets": "Ficheiros", "assets_added_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}}", "assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum", + "assets_added_to_albums_count": "Adicionado {assetTotal, plural, one {# asset} other {# assets}} a {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "Não foi possível adicionar {count, plural, one {ficheiro} other {ficheiros}} ao álbum", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} não pode ser adicionado a nenhum dos álbuns", "assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}", "assets_deleted_permanently": "{count} ficheiro(s) eliminado(s) permanentemente", "assets_deleted_permanently_from_server": "{count} ficheiro(s) eliminado(s) permanentemente do servidor Immich", @@ -511,14 +531,17 @@ "assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem", "assets_trashed_from_server": "{count} ficheiro(s) do servidor Immich foi/foram enviados para a reciclagem", "assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum", + "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} já faz parte dos álbuns", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes", "automatic_endpoint_switching_title": "Troca automática de URL", "autoplay_slideshow": "Apresentação automática de diapositivos", "back": "Voltar", "back_close_deselect": "Voltar, fechar ou desmarcar", + "background_backup_running_error": "Com a cópia de segurança de fundo em execução, não é possível inicar uma manual", "background_location_permission": "Permissão de localização em segundo plano", "background_location_permission_content": "Para que seja possível trocar a URL quando estiver executando em segundo plano, o Immich deve *sempre* ter a permissão de localização precisa para que o aplicativo consiga ler o nome da rede Wi-Fi", + "background_options": "Opções de fundo", "backup": "Cópia de segurança", "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para excluir", @@ -526,6 +549,7 @@ "backup_album_selection_page_select_albums": "Selecione Álbuns", "backup_album_selection_page_selection_info": "Informações da Seleção", "backup_album_selection_page_total_assets": "Total de arquivos únicos", + "backup_albums_sync": "Cópia de segurança de sincronização de álbuns", "backup_all": "Tudo", "backup_background_service_backup_failed_message": "Ocorreu um erro ao efetuar cópia de segurança dos ficheiros. A tentar de novo…", "backup_background_service_connection_failed_message": "Ocorreu um erro na ligação ao servidor. A tentar de novo…", @@ -585,8 +609,6 @@ "backup_setting_subtitle": "Gerenciar as configurações de envio em primeiro e segundo plano", "backup_settings_subtitle": "Gerir definições de carregamento", "backward": "Para trás", - "beta_sync": "Estado de Sincronização Beta", - "beta_sync_subtitle": "Gerir o novo sistema de sincronização", "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", @@ -644,6 +666,8 @@ "change_pin_code": "Alterar código PIN", "change_your_password": "Alterar a sua palavra-passe", "changed_visibility_successfully": "Visibilidade alterada com sucesso", + "charging": "A carregar", + "charging_requirement_mobile_backup": "Cópia de segurança de fundo necesssita que o dispositivo esteja a carregar", "check_corrupt_asset_backup": "Verificar por backups corrompidos", "check_corrupt_asset_backup_button": "Verificar", "check_corrupt_asset_backup_description": "Execute esta verificação somente em uma rede Wi-Fi e quando o backup de todos os arquivos já estiver concluído. O processo demora alguns minutos.", @@ -730,6 +754,7 @@ "create_user": "Criar utilizador", "created": "Criado", "created_at": "Criado a", + "creating_linked_albums": "A criar albuns ligados...", "crop": "Cortar", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", @@ -749,6 +774,7 @@ "date_of_birth_saved": "Data de nascimento guardada com sucesso", "date_range": "Intervalo de datas", "day": "Dia", + "days": "Dias", "deduplicate_all": "Remover todos os duplicados", "deduplication_criteria_1": "Tamanho da imagem em bytes", "deduplication_criteria_2": "Quantidade de dados EXIF", @@ -833,10 +859,12 @@ "edit": "Editar", "edit_album": "Editar álbum", "edit_avatar": "Editar imagem de perfil", - "edit_birthday": "Alterar aniversário", + "edit_birthday": "Editar aniversário", "edit_date": "Editar data", "edit_date_and_time": "Editar data e hora", "edit_date_and_time_action_prompt": "Alterada a data e hora de {count} ficheiros", + "edit_date_and_time_by_offset": "Alterar data com diferença", + "edit_date_and_time_by_offset_interval": "Novo período: {from} - {to}", "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", @@ -876,7 +904,9 @@ "error": "Erro", "error_change_sort_album": "Ocorreu um erro ao mudar a ordem de exibição", "error_delete_face": "Falha ao remover rosto do ficheiro", + "error_getting_places": "Erro ao obter locais", "error_loading_image": "Erro ao carregar a imagem", + "error_loading_partners": "Erro a carregar parceiros: {error}", "error_saving_image": "Erro: {error}", "error_tag_face_bounding_box": "Erro ao marcar o rosto - não foi possível localizar o rosto", "error_title": "Erro - Algo correu mal", @@ -909,6 +939,7 @@ "failed_to_load_notifications": "Ocorreu um erro ao carregar notificações", "failed_to_load_people": "Ocorreu um erro ao carregar pessoas", "failed_to_remove_product_key": "Ocorreu um erro ao remover chave de produto", + "failed_to_reset_pin_code": "Falha ao repor o código PIN", "failed_to_stack_assets": "Ocorreu um erro ao empilhar os ficheiros", "failed_to_unstack_assets": "Ocorreu um erro ao desempilhar ficheiros", "failed_to_update_notification_status": "Ocorreu um erro ao atualizar o estado das notificações", @@ -917,6 +948,7 @@ "paths_validation_failed": "Ocorreu um erro na validação de {paths, plural, one {# caminho} other {# caminhos}}", "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixeis transparentes. Por favor amplie e/ou mova a imagem.", "quota_higher_than_disk_size": "Definiu uma quota maior do que o tamanho do disco", + "something_went_wrong": "Algo deu errado", "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum", "unable_to_add_assets_to_shared_link": "Não foi possível adicionar os ficheiros ao link partilhado", "unable_to_add_comment": "Não foi possível adicionar o comentário", @@ -1039,6 +1071,7 @@ "favorites_page_no_favorites": "Nenhum favorito encontrado", "feature_photo_updated": "Foto principal atualizada", "features": "Funcionalidades", + "features_in_development": "Funcionalidades em Desenvolvimento", "features_setting_description": "Configurar as funcionalidades da aplicação", "file_name": "Nome do ficheiro", "file_name_or_extension": "Nome do ficheiro ou extensão", @@ -1048,21 +1081,26 @@ "filter_people": "Filtrar pessoas", "filter_places": "Filtrar lugares", "find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa", + "first": "Primeiro", "fix_incorrect_match": "Corrigir correspondência incorreta", "folder": "Pasta", "folder_not_found": "Pasta não encontrada", "folders": "Pastas", "folders_feature_description": "Navegar na vista de pastas por fotos e vídeos no sistema de ficheiros", + "forgot_pin_code_question": "Esqueceu o seu PIN?", "forward": "Para a frente", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade requer o carregamento de recursos externos da Google para poder funcionar.", "general": "Geral", + "geolocation_instruction_location": "Clique num ativo com coordenadas GPS para usar a sua localização ou seleccione um local diretamente do mapa", "get_help": "Obter Ajuda", "get_wifiname_error": "Não foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissões necessárias e se está conectado a uma rede Wi-Fi", "getting_started": "Primeiros Passos", "go_back": "Regressar", "go_to_folder": "Ir para a pasta", "go_to_search": "Ir para a pesquisa", + "gps": "GPS", + "gps_missing": "Sem GPS", "grant_permission": "Conceder permissão", "group_albums_by": "Agrupar álbuns por...", "group_country": "Agrupar por país", @@ -1107,6 +1145,7 @@ "home_page_upload_err_limit": "Só é possível enviar 30 arquivos por vez, ignorando", "host": "Servidor", "hour": "Hora", + "hours": "Horas", "id": "ID", "idle": "Em espera", "ignore_icloud_photos": "ignorar fotos no iCloud", @@ -1167,6 +1206,7 @@ "language_search_hint": "Procurar línguas...", "language_setting_description": "Selecione o seu Idioma preferido", "large_files": "Ficheiros Grandes", + "last": "Último", "last_seen": "Visto pela ultima vez", "latest_version": "Versão mais recente", "latitude": "Latitude", @@ -1185,6 +1225,7 @@ "library_page_sort_title": "Título do álbum", "licenses": "Licenças", "light": "Claro", + "like": "Gostar", "like_deleted": "Gosto removido", "link_motion_video": "Relacionar video animado", "link_to_oauth": "Link do OAuth", @@ -1195,6 +1236,7 @@ "local": "Local", "local_asset_cast_failed": "Não é possível transmitir um ficheiro que não tenha sido enviado antes para o servidor", "local_assets": "Ficheiros Locais", + "local_media_summary": "Sumário de conteúdo local", "local_network": "Rede local", "local_network_sheet_info": "O aplicativo irá se conectar ao servidor através desta URL quando estiver na rede Wi-Fi especificada", "location_permission": "Permissão de localização", @@ -1206,6 +1248,7 @@ "location_picker_longitude_hint": "Digite a longitude", "lock": "Trancar", "locked_folder": "Pasta Trancada", + "log_detail_title": "Detalhes de registo", "log_out": "Sair", "log_out_all_devices": "Terminar a sessão de todos os dispositivos", "logged_in_as": "Utilizador atual: {user}", @@ -1220,7 +1263,7 @@ "login_form_endpoint_url": "URL do servidor", "login_form_err_http": "Por favor especifique http:// ou https://", "login_form_err_invalid_email": "Email Inválido", - "login_form_err_invalid_url": "URL inválida", + "login_form_err_invalid_url": "URL inválido", "login_form_err_leading_whitespace": "Espaço em branco no início", "login_form_err_trailing_whitespace": "Espaço em branco no fim", "login_form_failed_get_oauth_server_config": "Ocorreu um erro ao iniciar sessão com o OAuth, verifique o URL do servidor", @@ -1236,6 +1279,7 @@ "login_password_changed_success": "Palavra-passe 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?", + "logs": "Logs", "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", @@ -1243,6 +1287,7 @@ "main_branch_warning": "Está a utilizar uma versão de desenvolvimento, recomendamos vivamente que utilize uma versão estável!", "main_menu": "Menu Principal", "make": "Marca", + "manage_geolocation": "Gerir localização", "manage_shared_links": "Gerir links partilhados", "manage_sharing_with_partners": "Gerir partilha com parceiros", "manage_the_app_settings": "Gerir definições da aplicação", @@ -1251,7 +1296,7 @@ "manage_your_devices": "Gerir os seus dispositivos com sessão iniciada", "manage_your_oauth_connection": "Gerir a sua ligação ao OAuth", "map": "Mapa", - "map_assets_in_bounds": "{count, plural, one {# foto} other {# fotos}}", + "map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}", "map_cannot_get_user_location": "Impossível obter a sua localização", "map_location_dialog_yes": "Sim", "map_location_picker_page_use_location": "Utilizar esta localização", @@ -1277,6 +1322,7 @@ "mark_as_read": "Marcar como lido", "marked_all_as_read": "Tudo marcado como lido", "matches": "Correspondências", + "matching_assets": "Conteúdos coincidentes", "media_type": "Tipo de média", "memories": "Memórias", "memories_all_caught_up": "Finalizamos por hoje", @@ -1295,6 +1341,7 @@ "merged_people_count": "Unidas {count, plural, one {# pessoa} other {# pessoas}}", "minimize": "Minimizar", "minute": "Minuto", + "minutes": "Minutos", "missing": "Em falta", "model": "Modelo", "month": "Mês", @@ -1314,6 +1361,10 @@ "my_albums": "Os meus álbuns", "name": "Nome", "name_or_nickname": "Nome ou alcunha", + "network_requirement_photos_upload": "Usar dados móveis para fazer backup de fotos", + "network_requirement_videos_upload": "Usar dados móveis para fazer backup de vídeos", + "network_requirements": "Requisitos de rede", + "network_requirements_updated": "Requisitos de rede alterados, redefinindo fila de backup", "networking_settings": "Conexões", "networking_subtitle": "Gerencie a conexão do servidor", "never": "Nunca", @@ -1323,6 +1374,7 @@ "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_timeline": "Nova Linha do Tempo", "new_user_created": "Novo utilizador criado", "new_version_available": "NOVA VERSÃO DISPONÍVEL", "newest_first": "Mais recente primeiro", @@ -1336,6 +1388,7 @@ "no_assets_message": "FAÇA CLIQUE PARA CARREGAR A SUA PRIMEIRA FOTO", "no_assets_to_show": "Não há arquivos para exibir", "no_cast_devices_found": "Nenhum dispositivo de transmissão encontrado", + "no_checksum_local": "Sem cálculo de verificação disponível - não pode capturar conteúdos locais", "no_duplicates_found": "Nenhum item duplicado foi encontrado.", "no_exif_info_available": "Sem informações exif disponíveis", "no_explore_results_message": "Carregue mais fotos para explorar a sua coleção.", @@ -1350,6 +1403,7 @@ "no_results_description": "Tente um sinónimo ou uma palavra-chave mais comum", "no_shared_albums_message": "Crie um álbum para partilhar fotos e vídeos com pessoas na sua rede", "no_uploads_in_progress": "Nenhum carregamento em curso", + "not_available": "N/A", "not_in_any_album": "Não está em nenhum álbum", "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", @@ -1365,6 +1419,7 @@ "oauth": "OAuth", "official_immich_resources": "Recursos oficiais do Immich", "offline": "Offline", + "offset": "Desvio", "ok": "Ok", "oldest_first": "Mais antigo primeiro", "on_this_device": "Neste dispositivo", @@ -1383,6 +1438,8 @@ "open_the_search_filters": "Abrir os filtros de pesquisa", "options": "Opções", "or": "ou", + "organize_into_albums": "Organizar em álbuns", + "organize_into_albums_description": "Colocar fotos existentes em álbuns utilizando as definições atuais de sincronização", "organize_your_library": "Organizar a sua biblioteca", "original": "original", "other": "Outro", @@ -1442,6 +1499,9 @@ "permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça backups e gerencie sua galeria, conceda permissões para fotos e vídeos nas configurações.", "permission_onboarding_request": "O Immich requer autorização para ver as suas fotos e vídeos.", "person": "Pessoa", + "person_age_months": "{months, plural, one {# month} other {# months}} idade", + "person_age_year_months": "1 ano, {months, plural, one {# month} other {# months}} idade", + "person_age_years": "{years, plural, other {# years}} idade", "person_birthdate": "Nasceu a {date}", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", "photo_shared_all_users": "Parece que partilhou as suas fotos com todos os utilizadores ou não tem nenhum utilizador para partilhar.", @@ -1465,6 +1525,7 @@ "port": "Porta", "preferences_settings_subtitle": "Gerenciar preferências do aplicativo", "preferences_settings_title": "Preferências", + "preparing": "A Preparar", "preset": "Predefinição", "preview": "Pré-visualizar", "previous": "Anterior", @@ -1481,6 +1542,7 @@ "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_readonly_mode": "Modo somente leitura ativado. Toque duas vezes no ícone do avatar do utilizador para sair.", "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}", @@ -1519,6 +1581,7 @@ "purchase_server_description_2": "Status de apoiante", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "A chave de produto do servidor é gerida pelo administrador", + "query_asset_id": "Consultar ID do recurso", "queue_status": "Em fila {count}/{total}", "rating": "Classificação por estrelas", "rating_clear": "Limpar classificação", @@ -1526,6 +1589,9 @@ "rating_description": "Mostrar a classificação EXIF no painel de informações", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", + "readonly_mode_disabled": "Modo somente leitura desativado", + "readonly_mode_enabled": "Modo somente leitura ativado", + "ready_for_upload": "Pronto para upload", "reassign": "Reatribuir", "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# ficheiro} other {# ficheiros}} para {name, select, null {uma pessoa existente} other {{name}}}", "reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa", @@ -1587,6 +1653,9 @@ "reset_password": "Redefinir palavra-passe", "reset_people_visibility": "Redefinir pessoas ocultas", "reset_pin_code": "Repor código PIN", + "reset_pin_code_description": "Se esqueceu o seu código PIN, pode entrar em contato com o administrador do servidor para o repor", + "reset_pin_code_success": "Código PIN redefinido com sucesso", + "reset_pin_code_with_password": "Pode sempre repor o seu código PIN com a sua senha", "reset_sqlite": "Reiniciar Base de Dados SQLite", "reset_sqlite_confirmation": "Tem a certeza de que quer reiniciar a base de dados SQLite? Vai ter de terminar a sessão e entrar outra vez para sincronizar os dados de novo", "reset_sqlite_success": "Base de dados SQLite reiniciada com sucesso", @@ -1599,8 +1668,10 @@ "restore_user": "Restaurar utilizador", "restored_asset": "Ficheiro restaurado", "resume": "Continuar", + "resume_paused_jobs": "Continuar {count, plural, one {# trabalho em pausa} other {# trabalhos pausados}}", "retry_upload": "Tentar carregar novamente", "review_duplicates": "Rever itens duplicados", + "review_large_files": "Rever arquivos grandes", "role": "Função", "role_editor": "Editor", "role_viewer": "Visualizador", @@ -1691,6 +1762,7 @@ "select_user_for_sharing_page_err_album": "Ocorreu um erro ao criar o álbum", "selected": "Selecionados", "selected_count": "{count, plural, other {# selecionados}}", + "selected_gps_coordinates": "Coordenadas GPS selecionadas", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server_endpoint": "URL do servidor", @@ -1758,6 +1830,7 @@ "shared_link_clipboard_copied_massage": "Copiado para a área de transferência", "shared_link_clipboard_text": "Ligação: {link}\nPalavra-passe: {password}", "shared_link_create_error": "Erro ao criar o link compartilhado", + "shared_link_custom_url_description": "Aceda a este link partilhado com um URL personalizado", "shared_link_edit_description_hint": "Digite a descrição do compartilhamento", "shared_link_edit_expire_after_option_day": "1 dia", "shared_link_edit_expire_after_option_days": "{count} dias", @@ -1783,6 +1856,7 @@ "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gerenciar links compartilhados", "shared_link_options": "Opções de link partilhado", + "shared_link_password_description": "Exigir uma senha para aceder a este link partilhado", "shared_links": "Links partilhados", "shared_links_description": "Partilhar fotos e videos com um link", "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos partilhados.}}", @@ -1817,6 +1891,7 @@ "show_slideshow_transition": "Mostrar transições no Modo de Apresentação", "show_supporter_badge": "Emblema de apoiante", "show_supporter_badge_description": "Mostrar um emblema de apoiante", + "show_text_search_menu": "Mostrar menu de pesquisa de texto", "shuffle": "Aleatório", "sidebar": "Barra lateral", "sidebar_display_description": "Mostrar um link para a vista na barra lateral", @@ -1832,6 +1907,7 @@ "sort_created": "Data de criação", "sort_items": "Número de itens", "sort_modified": "Data de modificação", + "sort_newest": "A foto mais recente", "sort_oldest": "Foto mais antiga", "sort_people_by_similarity": "Ordenar pessoas por semelhança", "sort_recent": "Foto mais recente", @@ -1846,6 +1922,7 @@ "stacktrace": "Stacktrace", "start": "Iniciar", "start_date": "Data de início", + "start_date_before_end_date": "A data de início deve ser anterior à data de fim", "state": "Estado/Distrito", "status": "Estado", "stop_casting": "Parar transmissão", @@ -1870,6 +1947,8 @@ "sync_albums_manual_subtitle": "Sincronizar todas as fotos e vídeos enviados para o álbum de backup selecionado", "sync_local": "Sincronização Local", "sync_remote": "Sincronização Remota", + "sync_status": "Status da sincronização", + "sync_status_subtitle": "Ver e gerir o sistema de sincronização", "sync_upload_album_setting_subtitle": "Crie e envie suas fotos e vídeos para o álbum selecionado no Immich", "tag": "Etiqueta", "tag_assets": "Etiquetar ficheiros", @@ -1907,7 +1986,9 @@ "to_change_password": "Alterar palavra-passe", "to_favorite": "Favorito", "to_login": "Iniciar Sessão", + "to_multi_select": "multi-selecção", "to_parent": "Subir um nível", + "to_select": "seleccionar", "to_trash": "Reciclagem", "toggle_settings": "Alternar configurações", "total": "Total", @@ -1927,6 +2008,7 @@ "trash_page_select_assets_btn": "Selecionar arquivos", "trash_page_title": "Reciclagem ({count})", "trashed_items_will_be_permanently_deleted_after": "Os itens da reciclagem são eliminados permanentemente após {days, plural, one {# dia} other {# dias}}.", + "troubleshoot": "Diagnosticar problemas", "type": "Tipo", "unable_to_change_pin_code": "Não foi possível alterar o código PIN", "unable_to_setup_pin_code": "Não foi possível configurar o código PIN", @@ -1957,6 +2039,7 @@ "unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}", "untagged": "Marcador removido", "up_next": "A seguir", + "update_location_action_prompt": "Atualize a localização de {count} ficheiros selecionados com:", "updated_at": "Atualizado a", "updated_password": "Palavra-passe atualizada", "upload": "Carregar", @@ -2023,6 +2106,7 @@ "view_next_asset": "Ver próximo ficheiro", "view_previous_asset": "Ver ficheiro anterior", "view_qr_code": "Ver código QR", + "view_similar_photos": "Ver fotos similares", "view_stack": "Ver pilha", "view_user": "Ver utilizador", "viewer_remove_from_stack": "Remover da pilha", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 3641a95d87..03471cef46 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -28,6 +28,10 @@ "add_to_album": "Adicionar ao álbum", "add_to_album_bottom_sheet_added": "Adicionado ao {album}", "add_to_album_bottom_sheet_already_exists": "Já existe em {album}", + "add_to_album_bottom_sheet_some_local_assets": "Alguns arquivos / mídias não puderam ser adicionados ao álbum", + "add_to_album_toggle": "Alternar a seleção de {album}", + "add_to_albums": "Adicionar aos álbuns", + "add_to_albums_count": "Adicionar aos álbuns ({count})", "add_to_shared_album": "Adicionar ao álbum compartilhado", "add_url": "Adicionar URL", "added_to_archive": "Adicionado ao arquivo", @@ -120,6 +124,13 @@ "logging_enable_description": "Habilitar logs", "logging_level_description": "Quando ativado, qual nível de log usar.", "logging_settings": "Logs", + "machine_learning_availability_checks": "Verficações de disponibilidade", + "machine_learning_availability_checks_description": "Automaticamente detectar e preferir servidores de machine learning disponíveis", + "machine_learning_availability_checks_enabled": "Habilitar verificações de disponibilidade", + "machine_learning_availability_checks_interval": "Intervalo de verificação", + "machine_learning_availability_checks_interval_description": "Intervalo em milisegundos entre verificações de disponibilidade", + "machine_learning_availability_checks_timeout": "Tempo limite da solicitação", + "machine_learning_availability_checks_timeout_description": "Tempo limite em milisegundos para verificações de disponibilidade", "machine_learning_clip_model": "Modelo CLIP", "machine_learning_clip_model_description": "O nome de um modelo CLIP listado aqui. Lembre-se de executar novamente a tarefa de 'Pesquisa Inteligente' para todas as imagens após alterar o modelo.", "machine_learning_duplicate_detection": "Detecção de duplicidade", @@ -384,8 +395,6 @@ "admin_password": "Senha do administrador", "administration": "Administração", "advanced": "Avançado", - "advanced_settings_beta_timeline_subtitle": "Teste a nova interface do aplicativo", - "advanced_settings_beta_timeline_title": "Linha do tempo Beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Use esta opção para filtrar mídias durante a sincronização com base em critérios alternativos. Tente esta opção somente se o aplicativo estiver com problemas para detectar todos os álbuns.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar filtro alternativo de sincronização de álbum de dispositivo", "advanced_settings_log_level_title": "Nível de log: {level}", @@ -393,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Preferir imagens do servidor", "advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede", "advanced_settings_proxy_headers_title": "Cabeçalhos do Proxy", + "advanced_settings_readonly_mode_subtitle": "Ativar o modo de apenas visualização dos arquivos. As outras ações, como: selecionar várias imagens, compartilhar, transmitir ou deletar serão desabilitadas. Ative ou Desative este modo clicando na foto do usuário na tela principal", + "advanced_settings_readonly_mode_title": "Modo de apenas visualização", "advanced_settings_self_signed_ssl_subtitle": "Ignora a verificação do certificado SSL do servidor. Obrigatório para certificados auto assinados.", "advanced_settings_self_signed_ssl_title": "Permitir certificados SSL auto assinados", "advanced_settings_sync_remote_deletions_subtitle": "Excluir ou restaurar os arquivos automaticamente neste dispositivo quando essas ações forem realizada na interface web", @@ -420,6 +431,7 @@ "album_remove_user_confirmation": "Tem certeza de que deseja remover {user}?", "album_search_not_found": "Não há álbum que corresponda à sua pesquisa", "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_summary": "Resumo do álbum", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos recursos", "album_user_left": "Saiu do álbum {album}", @@ -458,6 +470,7 @@ "app_bar_signout_dialog_title": "Sair", "app_settings": "Configurações do Aplicativo", "appears_in": "Aparece em", + "apply_count": "Aplicar ({count, number})", "archive": "Arquivar", "archive_action_prompt": "{count} mídias arquivadas", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", @@ -490,6 +503,8 @@ "asset_restored_successfully": "Arquivo restaurado", "asset_skipped": "Ignorado", "asset_skipped_in_trash": "Na lixeira", + "asset_trashed": "Arquivo enviado para a lixeira", + "asset_troubleshoot": "Diagnóstico do arquivo", "asset_uploaded": "Enviado", "asset_uploading": "Enviando…", "asset_viewer_settings_subtitle": "Gerenciar as configurações do visualizador da galeria", @@ -497,7 +512,9 @@ "assets": "Arquivos", "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}", "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum", + "assets_added_to_albums_count": "{assetTotal, plural, one {# Arquivo adicionado} other {# Arquivos adicionados}} {albumTotal, plural, one {# ao álbum} other {# aos álbuns}}", "assets_cannot_be_added_to_album_count": "Não foi possível adicionar {count, plural, one {o arquivo} other {os arquivos}} ao álbum", + "assets_cannot_be_added_to_albums": "{count, plural, one {Arquivo não pode ser adicionado} other {Arquivos não podem ser adicionados}} a nenhum álbum", "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}", "assets_deleted_permanently": "{count} arquivo(s) deletado(s) permanentemente", "assets_deleted_permanently_from_server": "{count} arquivo(s) deletado(s) permanentemente do servidor Immich", @@ -514,14 +531,17 @@ "assets_trashed_count": "{count, plural, one {# arquivo movido para a lixeira} other {# arquivos movidos para a lixeira}}", "assets_trashed_from_server": "{count} arquivos foram enviados para a lixeira", "assets_were_part_of_album_count": "{count, plural, one {O arquivo já faz} other {Os arquivos já fazem}} parte do álbum", + "assets_were_part_of_albums_count": "{count, plural, one {Arquivo já existe} other {Arquivos já existem}} nos álbuns", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes", "automatic_endpoint_switching_title": "Troca automática de URL", "autoplay_slideshow": "Apresentação de slides automática", "back": "Voltar", "back_close_deselect": "Voltar, fechar ou desmarcar", + "background_backup_running_error": "Não é possível iniciar o backup manual agora pois o backup em segundo plano já está sendo executado", "background_location_permission": "Permissão de localização em segundo plano", "background_location_permission_content": "Para que seja possível trocar o endereço quando estiver executando em segundo plano, o Immich deve *sempre* ter a permissão de localização precisa para que o aplicativo consiga ler o nome da rede Wi-Fi", + "background_options": "Opções de Plano de Fundo", "backup": "Backup", "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir", @@ -529,6 +549,7 @@ "backup_album_selection_page_select_albums": "Selecionar álbuns", "backup_album_selection_page_selection_info": "Informações da Seleção", "backup_album_selection_page_total_assets": "Total de recursos exclusivos", + "backup_albums_sync": "Backup de sincronização de álbuns", "backup_all": "Todos", "backup_background_service_backup_failed_message": "Falha ao fazer backup. Tentando novamente…", "backup_background_service_connection_failed_message": "Falha na conexão com o servidor. Tentando novamente…", @@ -588,8 +609,6 @@ "backup_setting_subtitle": "Gerenciar as configurações de envio em primeiro e segundo plano", "backup_settings_subtitle": "Gerenciar configurações de envio", "backward": "Para trás", - "beta_sync": "Status da sincronização Beta", - "beta_sync_subtitle": "Configurar o novo sistema de sincronização", "biometric_auth_enabled": "Autenticação por biometria ativada", "biometric_locked_out": "Sua autenticação por biometria está bloqueada", "biometric_no_options": "Não há opções de biometria disponíveis", @@ -647,6 +666,8 @@ "change_pin_code": "Alterar código PIN", "change_your_password": "Alterar sua senha", "changed_visibility_successfully": "Visibilidade alterada com sucesso", + "charging": "Carregando", + "charging_requirement_mobile_backup": "Backups em plano de fundo requerem que o dispositivo esteja sendo carregado", "check_corrupt_asset_backup": "Verifique se há backups corrompidos", "check_corrupt_asset_backup_button": "Verificar", "check_corrupt_asset_backup_description": "Execute esta verificação somente em uma rede Wi-Fi e quando o backup de todos os arquivos já estiver concluído. O processo demora alguns minutos.", @@ -733,6 +754,7 @@ "create_user": "Criar usuário", "created": "Criado", "created_at": "Criado em", + "creating_linked_albums": "Criando álbuns relacionados...", "crop": "Cortar", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", @@ -882,7 +904,9 @@ "error": "Erro", "error_change_sort_album": "Falha ao alterar a ordem de exibição", "error_delete_face": "Erro ao remover face do arquivo", + "error_getting_places": "Erro ao buscar os locais", "error_loading_image": "Erro ao carregar a página", + "error_loading_partners": "Erro ao carregar parceiros: {error}", "error_saving_image": "Erro: {error}", "error_tag_face_bounding_box": "Erro ao marcar o rosto - não foi possível localizar o rosto", "error_title": "Erro - Algo deu errado", @@ -1047,6 +1071,7 @@ "favorites_page_no_favorites": "Nenhuma mídia favorita encontrada", "feature_photo_updated": "Foto principal atualizada", "features": "Funcionalidades", + "features_in_development": "Funções em desenvolvimento", "features_setting_description": "Gerenciar as funcionalidades da aplicação", "file_name": "Nome do arquivo", "file_name_or_extension": "Nome do arquivo ou extensão", @@ -1056,6 +1081,7 @@ "filter_people": "Filtrar pessoas", "filter_places": "Filtrar lugares", "find_them_fast": "Encontre pelo nome em uma pesquisa", + "first": "Primeiro", "fix_incorrect_match": "Corrigir correspondência incorreta", "folder": "Pasta", "folder_not_found": "Pasta não encontrada", @@ -1066,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade carrega recursos externos do Google para funcionar.", "general": "Geral", + "geolocation_instruction_location": "Selecione um arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa", "get_help": "Obter Ajuda", "get_wifiname_error": "Não foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissões necessárias e se está conectado a uma rede Wi-Fi", "getting_started": "Primeiros passos", "go_back": "Voltar", "go_to_folder": "Ir para a pasta", "go_to_search": "Ir para a pesquisa", + "gps": "GPS", + "gps_missing": "Sem GPS", "grant_permission": "Conceder permissão", "group_albums_by": "Agrupar álbuns por...", "group_country": "Agrupar por país", @@ -1177,6 +1206,7 @@ "language_search_hint": "Procure idiomas...", "language_setting_description": "Selecione seu Idioma preferido", "large_files": "Arquivos Grandes", + "last": "Último", "last_seen": "Visto pela ultima vez", "latest_version": "Versão mais recente", "latitude": "Latitude", @@ -1206,6 +1236,7 @@ "local": "Local", "local_asset_cast_failed": "Não é possível transmitir um arquivo que não foi enviado ao servidor", "local_assets": "Arquivos no dispositivo", + "local_media_summary": "Resumo das mídias locais", "local_network": "Rede local", "local_network_sheet_info": "O aplicativo irá se conectar ao servidor através deste endereço quando estiver na rede Wi-Fi especificada", "location_permission": "Permissão de localização", @@ -1217,6 +1248,7 @@ "location_picker_longitude_hint": "Digite a longitude", "lock": "Trancar", "locked_folder": "Pasta com senha", + "log_detail_title": "Detalhes do Log", "log_out": "Sair", "log_out_all_devices": "Sair de todos dispositivos", "logged_in_as": "Usuário atual: {user}", @@ -1247,6 +1279,7 @@ "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?", + "logs": "Logs", "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", @@ -1254,6 +1287,7 @@ "main_branch_warning": "Você está utilizando uma versão de desenvolvimento. É fortemente recomendado que utilize uma versão estável!", "main_menu": "Menu Principal", "make": "Marca", + "manage_geolocation": "Gerenciar localização", "manage_shared_links": "Gerir links partilhados", "manage_sharing_with_partners": "Gerenciar compartilhamento com parceiros", "manage_the_app_settings": "Gerenciar configurações do app", @@ -1288,6 +1322,7 @@ "mark_as_read": "Marcar como lido", "marked_all_as_read": "Tudo marcado como lido", "matches": "Correspondências", + "matching_assets": "Arquivos encontrados", "media_type": "Tipo de mídia", "memories": "Memórias", "memories_all_caught_up": "Finalizamos por hoje", @@ -1328,6 +1363,7 @@ "name_or_nickname": "Nome ou apelido", "network_requirement_photos_upload": "Use a rede móvel para enviar fotos", "network_requirement_videos_upload": "Use a rede móvel para enviar vídeos", + "network_requirements": "Requerimentos de Rede", "network_requirements_updated": "Requerimentos de rede alterados, reiniciando a fila de envio", "networking_settings": "Conexões", "networking_subtitle": "Gerencie as conexões ao servidor", @@ -1338,6 +1374,7 @@ "new_person": "Nova Pessoa", "new_pin_code": "Novo código PIN", "new_pin_code_subtitle": "Esta é a primeira vez que está acessando a pasta com senha. Crie um código PIN para acessar esta página de forma segura", + "new_timeline": "Nova Linha do Tempo", "new_user_created": "Novo usuário criado", "new_version_available": "NOVA VERSÃO DISPONÍVEL", "newest_first": "Mais recente primeiro", @@ -1351,20 +1388,25 @@ "no_assets_message": "CLIQUE PARA ENVIAR SUA PRIMEIRA FOTO", "no_assets_to_show": "Não há arquivos para exibir", "no_cast_devices_found": "Nenhum dispositivo encontrado", + "no_checksum_local": "Nenhum checksum disponível - não foi possível carregar os arquivos locais", + "no_checksum_remote": "Nenhum checksum disponível - não foi possível carregar os arquivos remotos", "no_duplicates_found": "Nenhuma duplicidade foi encontrada.", "no_exif_info_available": "Sem informações exif disponíveis", "no_explore_results_message": "Envie mais fotos para explorar sua coleção.", "no_favorites_message": "Adicione aos favoritos para encontrar suas melhores fotos e vídeos rapidamente", "no_libraries_message": "Crie uma biblioteca externa para ver suas fotos e vídeos", + "no_local_assets_found": "Nenhum arquivo local foi encontrado com este checksum", "no_locked_photos_message": "Fotos e vídeos na pasta com senha são ocultos e não serão exibidos enquanto explora ou pesquisa na biblioteca.", "no_name": "Sem Nome", "no_notifications": "Nenhuma notificação", "no_people_found": "Nenhuma pessoa encontrada", "no_places": "Sem lugares", + "no_remote_assets_found": "Nenhum arquivo remoto foi encontrado com este checksum", "no_results": "Sem resultados", "no_results_description": "Tente um sinônimo ou uma palavra-chave mais geral", "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede", "no_uploads_in_progress": "Nenhum envio em progresso", + "not_available": "N/A", "not_in_any_album": "Fora de álbum", "not_selected": "Não selecionado", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos enviados anteriormente, execute o", @@ -1399,6 +1441,8 @@ "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", "or": "ou", + "organize_into_albums": "Organizar em álbuns", + "organize_into_albums_description": "Colocar imagens existentes em álbuns usando as configurações de sincronização atuais", "organize_your_library": "Organize sua biblioteca", "original": "original", "other": "Outro", @@ -1484,6 +1528,7 @@ "port": "Porta", "preferences_settings_subtitle": "Gerenciar as preferências do aplicativo", "preferences_settings_title": "Preferências", + "preparing": "Preparando", "preset": "Predefinição", "preview": "Pré-visualizar", "previous": "Anterior", @@ -1500,6 +1545,7 @@ "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_readonly_mode": "Modo apenas leitura habilidato. Dê um toque prolongado na foto do usuário para sair deste modo.", "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}", @@ -1538,6 +1584,7 @@ "purchase_server_description_2": "Status de Contribuidor", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "A chave do produto para servidor é gerenciada pelo administrador", + "query_asset_id": "Consultar ID do Ativo", "queue_status": "Na fila {count} de {total}", "rating": "Estrelas", "rating_clear": "Limpar classificação", @@ -1545,6 +1592,9 @@ "rating_description": "Exibir o EXIF de classificação no painel de informações", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", + "readonly_mode_disabled": "Modo apenas visualização desativado", + "readonly_mode_enabled": "Modo apenas visualização ativado", + "ready_for_upload": "Pronto para upload", "reassign": "Reatribuir", "reassigned_assets_to_existing_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a {name, select, null {uma pessoa} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa", @@ -1569,6 +1619,7 @@ "regenerating_thumbnails": "Regenerando miniaturas", "remote": "Remoto", "remote_assets": "Arquivos Remotos", + "remote_media_summary": "Resumo das mídias remotas", "remove": "Remover", "remove_assets_album_confirmation": "Tem certeza de que deseja remover {count, plural, one {# arquivo} other {# arquivos}} do álbum?", "remove_assets_shared_link_confirmation": "Tem certeza de que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?", @@ -1621,6 +1672,7 @@ "restore_user": "Restaurar usuário", "restored_asset": "Arquivo restaurado", "resume": "Continuar", + "resume_paused_jobs": "Retomar {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Tentar enviar novamente", "review_duplicates": "Revisar duplicidade", "review_large_files": "Ver arquivos grandes", @@ -1714,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Falha ao criar álbum", "selected": "Selecionados", "selected_count": "{count, plural, one {# selecionado} other {# selecionados}}", + "selected_gps_coordinates": "Coordenadas de GPS Selecionada", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server_endpoint": "URL do servidor", @@ -1842,6 +1895,7 @@ "show_slideshow_transition": "Usar transições no modo de apresentação", "show_supporter_badge": "Insígnia de apoiador", "show_supporter_badge_description": "Mostrar uma insígnia de apoiador", + "show_text_search_menu": "Mostrar menu de pesquisa por texto", "shuffle": "Aleatório", "sidebar": "Barra lateral", "sidebar_display_description": "Exibir um link para a visualização na barra lateral", @@ -1857,7 +1911,7 @@ "sort_created": "Data de criação", "sort_items": "Número de itens", "sort_modified": "Data de modificação", - "sort_newest": "Foto mais recente", + "sort_newest": "Foto mais nova", "sort_oldest": "Foto mais antiga", "sort_people_by_similarity": "Ordenar pessoas por semelhança", "sort_recent": "Foto mais recente", @@ -1872,6 +1926,7 @@ "stacktrace": "Stacktrace", "start": "Início", "start_date": "Data inicial", + "start_date_before_end_date": "A data de início deve ser antes da data final", "state": "Estado", "status": "Status", "stop_casting": "Parar transmissão", @@ -1896,6 +1951,8 @@ "sync_albums_manual_subtitle": "Sincronize todos as fotos e vídeos enviados para os álbuns de backup selecionados", "sync_local": "Sincronização Local", "sync_remote": "Sincronização Remota", + "sync_status": "Status da Sincronização", + "sync_status_subtitle": "Ver e gerenciar o sistema de sincronização", "sync_upload_album_setting_subtitle": "Crie e envie suas fotos e vídeos para o álbum selecionado no Immich", "tag": "Marcador", "tag_assets": "Marcar arquivos", @@ -1933,7 +1990,9 @@ "to_change_password": "Alterar senha", "to_favorite": "Favorito", "to_login": "Iniciar sessão", + "to_multi_select": "selecionar vários", "to_parent": "Voltar para nível acima", + "to_select": "selecionar", "to_trash": "Mover para a lixeira", "toggle_settings": "Alternar configurações", "total": "Total", @@ -1953,6 +2012,7 @@ "trash_page_select_assets_btn": "Selecionar arquivos", "trash_page_title": "Lixeira ({count})", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira serão deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", + "troubleshoot": "Diagnosticar", "type": "Tipo", "unable_to_change_pin_code": "Não foi possível alterar o código PIN", "unable_to_setup_pin_code": "Não foi possível criar o código PIN", @@ -1983,6 +2043,7 @@ "unstacked_assets_count": "{count, plural, one {# arquivo retirado} other {# arquivos retirados}} do grupo", "untagged": "Marcador removido", "up_next": "A seguir", + "update_location_action_prompt": "Atualizar a localização de {count} arquivos selecionados para:", "updated_at": "Atualizado em", "updated_password": "Senha atualizada", "upload": "Enviar", @@ -2049,6 +2110,7 @@ "view_next_asset": "Ver próximo arquivo", "view_previous_asset": "Ver arquivo anterior", "view_qr_code": "Ver QR Code", + "view_similar_photos": "Ver fotos similares", "view_stack": "Ver grupo", "view_user": "Visualizar usuário", "viewer_remove_from_stack": "Remover do grupo", @@ -2067,5 +2129,6 @@ "yes": "Sim", "you_dont_have_any_shared_links": "Não há links compartilhados", "your_wifi_name": "Nome do seu Wi-Fi", - "zoom_image": "Ampliar imagem" + "zoom_image": "Ampliar imagem", + "zoom_to_bounds": "Ampliar para preencher" } diff --git a/i18n/ro.json b/i18n/ro.json index 8ca0b87ef0..76e87c0cb6 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -1,12 +1,12 @@ { "about": "Despre", "account": "Cont", - "account_settings": "Setări Cont", + "account_settings": "Setări cont", "acknowledge": "Văzut", "action": "Acţiune", "action_common_update": "Actualizează", "actions": "Acţiuni", - "active": "Activ", + "active": "Active", "activity": "Activitate", "activity_changed": "Activitatea este {enabled, select, true {activată} other {dezactivată}}", "add": "Adaugă", @@ -28,6 +28,10 @@ "add_to_album": "Adaugă în album", "add_to_album_bottom_sheet_added": "Adăugat în {album}", "add_to_album_bottom_sheet_already_exists": "Deja în {album}", + "add_to_album_bottom_sheet_some_local_assets": "Unele resurse locale nu au putut fi adăugate la album", + "add_to_album_toggle": "Selectează/deselectează {album}", + "add_to_albums": "Adaugă la albume", + "add_to_albums_count": "Adaugă la albume ({count})", "add_to_shared_album": "Adaugă la album partajat", "add_url": "Adăugați adresa URL", "added_to_archive": "Adăugat la arhivă", @@ -37,12 +41,12 @@ "add_exclusion_pattern_description": "Adăugați modele de excludere. Globing folosind *, ** și ? este suportat. Pentru a ignora toate fișierele din orice director numit „Raw”, utilizați „**/Raw/**”. Pentru a ignora toate fișierele care se termină în „.tif”, utilizați „**/*.tif”. Pentru a ignora o cale absolută, utilizați „/path/to/ignore/**”.", "admin_user": "Utilizator admin", "asset_offline_description": "Acest material din biblioteca externă nu se mai găsește pe disc și a fost mutat în coșul de gunoi. Dacă fișierul a fost mutat în bibliotecă, verificați cronologia pentru noul material corespunzător. Pentru a restabili acest material, asigurați-vă că calea fișierului de mai jos poate fi accesată de Immich și scanați biblioteca.", - "authentication_settings": "Setări de Autentificare", + "authentication_settings": "Setări de autentificare", "authentication_settings_description": "Gestionează parola, OAuth și alte setări de autentificare", "authentication_settings_disable_all": "Ești sigur că vrei sa dezactivezi toate metodele de autentificare? Autentificarea va fi complet dezactivată.", "authentication_settings_reenable": "Pentru a reactiva, folosește Comandă Server.", "background_task_job": "Activități de Fundal", - "backup_database": "Salvare Bază de Date", + "backup_database": "Salvare bază de date", "backup_database_enable_description": "Activare salvarea bazei de date", "backup_keep_last_amount": "Număr de copii de rezervă anterioare de păstrat", "backup_onboarding_1_description": "copie externă în cloud sau într-o altă locație fizică.", @@ -69,9 +73,9 @@ "disable_login": "Dezactivați autentificarea", "duplicate_detection_job_description": "Rulați învățarea automată pe materiale pentru a detecta imagini similare. Se bazează pe Căutare Inteligentă", "exclusion_pattern_description": "Modelele de excludere vă permit să ignorați fișierele și folderele atunci când vă scanați biblioteca. Acest lucru este util dacă aveți foldere care conțin fișiere pe care nu doriți să le importați, cum ar fi fișierele RAW.", - "external_library_management": "Managementul Bibliotecii Externe", + "external_library_management": "Gestionarea bibliotecilor externe", "face_detection": "Detecție facială", - "face_detection_description": "Detectează fețele din fișiere folosind învățare automată. Pentru videoclipuri, este luată în considerare doar miniatura. „Reînprospătează” (re)procesează toate fișierele. „Resetează” adaugă în coadă fișierele care nu au fost încă procesate. Fețele detectate vor fi puse în coadă pentru recunoașterea facială după finalizarea detectării feței, grupându-le în persoane existente sau noi.", + "face_detection_description": "Detectează fețele din fișiere folosind învățare automată. Pentru videoclipuri, este luată în considerare doar miniatura. „Reîmprospătează” (re)procesează toate fișierele. „Resetează” adaugă în coadă fișierele care nu au fost încă procesate. Fețele detectate vor fi puse în coadă pentru recunoașterea facială după finalizarea detectării feței, grupându-le în persoane existente sau noi.", "facial_recognition_job_description": "Grupați fețele detectate în persoane. Acest pas rulează după ce Detectarea Feței este finalizată. „Resetează” (re)grupează toate fețele. „Lipsă” adaugă în coadă fețe care nu au o persoană desemnată.", "failed_job_command": "Comanda {command} a eșuat pentru jobul: {job}", "force_delete_user_warning": "AVERTISMENT: Acest lucru va elimina imediat utilizatorul și toate activele sale. Acest lucru nu poate fi anulat și fișierele nu pot fi recuperate.", @@ -88,30 +92,30 @@ "image_prefer_wide_gamut_setting_description": "Utilizați Display P3 pentru miniaturi. Acest lucru păstrează mai bine vibrația imaginilor cu spații de culoare largi, dar imaginile pot apărea diferit pe dispozitivele cu o versiune mai veche de browser. Imaginile sRGB sunt păstrate ca sRGB pentru a evita schimbările de culoare.", "image_preview_description": "Imagine de dimensiune medie cu metadate eliminate, utilizată la vizualizarea unui singur element și pentru învățarea automată", "image_preview_quality_description": "Calitatea previzualizării de la 1 la 100. O valoare mai mare oferă o calitate mai bună, dar produce fișiere mai mari și poate reduce receptivitatea aplicației. Setarea unei valori scăzute poate afecta calitatea învățării automate.", - "image_preview_title": "Previzualizați Setările", + "image_preview_title": "Previzualizați setările", "image_quality": "Calitate", "image_resolution": "Rezolutie", "image_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru a fi codificate, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.", - "image_settings": "Setări Imagine", + "image_settings": "Setări imagine", "image_settings_description": "Gestionează calitatea și rezoluția imaginilor generate", "image_thumbnail_description": "Miniatură mică cu metadate eliminate, utilizată la vizualizarea grupurilor de fotografii, cum ar fi în cronologia principală", "image_thumbnail_quality_description": "Calitatea miniaturii de la 1 la 100. O valoare mai mare oferă o calitate mai bună, dar produce fișiere mai mari și poate reduce receptivitatea aplicației.", - "image_thumbnail_title": "Setari Miniaturi", + "image_thumbnail_title": "Setari miniaturi", "job_concurrency": "Concurență {job}", "job_created": "Sarcină creată", "job_not_concurrency_safe": "Această sarcină nu este sigură pentru a rula în concurență.", - "job_settings": "Setări Sarcină", + "job_settings": "Setări sarcină", "job_settings_description": "Administrează concurența sarcinilor", - "job_status": "Starea Sarcinii", + "job_status": "Starea sarcinii", "jobs_delayed": "{jobCount, plural, other {# întârziat}}", "jobs_failed": "{jobCount, plural, other {# eșuat}}", - "library_created": "Librărie creată:{library}", + "library_created": "Librărie creată: {library}", "library_deleted": "Bibliotecă ștearsă", "library_import_path_description": "Specificați un folder pentru a îl importa. Acest folder, inclusiv sub-folderele, vor fi scanate pentru imagini și videoclipuri.", - "library_scanning": "Scanare Periodică", + "library_scanning": "Scanare periodică", "library_scanning_description": "Configurează scanarea periodică pentru bibliotecă", "library_scanning_enable_description": "Activează scanarea periodică pentru bibliotecă", - "library_settings": "Bibliotecă Externă", + "library_settings": "Bibliotecă externă", "library_settings_description": "Administrează setările pentru biblioteci externe", "library_tasks_description": "Scanează bibliotecile externe de active noi sau modificate", "library_watching_enable_description": "Urmărește bibliotecile externe pentru schimbări ale fișierelor", @@ -120,9 +124,16 @@ "logging_enable_description": "Activează înregistrarea log-urilor", "logging_level_description": "Dacă setarea este activată, înregistrează evenimentele cu nivelul de utilizat.", "logging_settings": "Înregistrare", + "machine_learning_availability_checks": "Verificări disponibilitate", + "machine_learning_availability_checks_description": "Detectează automat si preferă serverele cu învațare automată", + "machine_learning_availability_checks_enabled": "Activează verificare disponibilitate", + "machine_learning_availability_checks_interval": "Interval verificare", + "machine_learning_availability_checks_interval_description": "Interval in milisecunde între verificările de disponibilitate", + "machine_learning_availability_checks_timeout": "Timp de expirare cerere", + "machine_learning_availability_checks_timeout_description": "Timp de așteptare în milisecunde pentru verificările de disponibilitate", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "Numele unui model CLIP listat aici. Rețineți că trebuie să rulați din nou funcția „Smart Search” pentru toate imaginile la schimbarea unui model.", - "machine_learning_duplicate_detection": "Detectare Duplicate", + "machine_learning_duplicate_detection": "Detectare duplicate", "machine_learning_duplicate_detection_enabled": "Activează detectarea duplicatelor", "machine_learning_duplicate_detection_enabled_description": "Dacă este dezactivată, elementele identice vor fi în continuare de-duplicate.", "machine_learning_duplicate_detection_setting_description": "Utilizați încorporările CLIP pentru a găsi dubluri probabile", @@ -149,11 +160,11 @@ "machine_learning_smart_search_enabled": "Activați căutarea inteligentă", "machine_learning_smart_search_enabled_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru căutarea inteligentă.", "machine_learning_url_description": "URL-ul serverului de învățare automată. Dacă sunt furnizate mai multe URL-uri, fiecare server va fi încercat pe rând, până când unul răspunde cu succes, în ordine de la primul până la ultimul. Serverele care nu răspund vor fi ignorate temporar până revin online.", - "manage_concurrency": "Gestionarea Simultaneității", + "manage_concurrency": "Gestionarea simultaneității", "manage_log_settings": "Administrați setările jurnalului", "map_dark_style": "Mod întunecat", "map_enable_description": "Activați funcțiile hărții", - "map_gps_settings": "Setări Hartă & GPS", + "map_gps_settings": "Setări hartă & GPS", "map_gps_settings_description": "Gestionare setări Hartă & GPS (localizare inversă)", "map_implications": "Caracteristica hărții se bazează pe un serviciu extern de planșe (tiles.immich.cloud)", "map_light_style": "Mod deschis", @@ -170,7 +181,7 @@ "metadata_extraction_job_description": "Extragere informații metadate din fiecare fișier cum ar fi localizare GPS, fețe și rezoluție,", "metadata_faces_import_setting": "Activare import fețe", "metadata_faces_import_setting_description": "Importă fețe din datele EXIF ale imaginii și din fișiere tip \"sidecar\"", - "metadata_settings": "Setări Metadate", + "metadata_settings": "Setări metadate", "metadata_settings_description": "Gestionează setările pentru metadate", "migration_job": "Migrare", "migration_job_description": "Migrați miniaturile pentru elemente și fețe la cea mai recentă structură de foldere", @@ -181,6 +192,7 @@ "nightly_tasks_generate_memories_setting": "Generare memorii", "nightly_tasks_generate_memories_setting_description": "Creează amintiri noi din resurse", "nightly_tasks_missing_thumbnails_setting": "Generează miniaturi lipsă", + "nightly_tasks_missing_thumbnails_setting_description": "Pune în coadă elementele fără miniaturi pentru generarea miniaturilor", "nightly_tasks_settings": "Setări pentru sarcinile nocturne", "nightly_tasks_settings_description": "Gestionați sarcinile nocturne", "nightly_tasks_start_time_setting": "Ora de începere", @@ -247,7 +259,7 @@ "send_welcome_email": "Trimite email de bun-venit", "server_external_domain_settings": "Domeniu extern", "server_external_domain_settings_description": "Domeniu pentru distribuire publicǎ a scurtǎturilor, incluzând http(s)://", - "server_public_users": "Utilizatori Publici", + "server_public_users": "Utilizatori publici", "server_public_users_description": "Toți utilizatorii (nume și e-mail) sunt listați atunci când adăugați un utilizator la albumele partajate. Când este dezactivată, lista de utilizatori va fi disponibilă numai pentru utilizatorii admin.", "server_settings": "Setǎri Server", "server_settings_description": "Gestioneazǎ setǎrile serverului", @@ -269,7 +281,7 @@ "storage_template_more_details": "Pentru mai multe detalii despre aceasta caracteristică, accesați Șablon stocare si implicațiile", "storage_template_onboarding_description_v2": "Când este activată, această funcție va organiza automat fișierele pe baza șablonului definit de către utilizator. Pentru mai multe informații, accesează documentația.", "storage_template_path_length": "Limita de lungime pentru calea aproximativă: {length, number}/{limit, number}", - "storage_template_settings": "Șablon Stocare", + "storage_template_settings": "Șablon stocare", "storage_template_settings_description": "Gestionează structura folderelor și numele fișierelor pentru elementele încărcate", "storage_template_user_label": "{label} este eticheta de stocare a utilizatorului", "system_settings": "Setǎri de Sistem", @@ -285,9 +297,9 @@ "template_settings_description": "Gestionați șabloanele personalizate pentru notificări", "theme_custom_css_settings": "CSS personalizat", "theme_custom_css_settings_description": "Foile de stil în cascadă (CSS) permit personalizarea designului Immich.", - "theme_settings": "Setări Temă", + "theme_settings": "Setări temă", "theme_settings_description": "Gestionează personalizarea interfeței web Immich", - "thumbnail_generation_job": "Generare Miniaturi", + "thumbnail_generation_job": "Generare miniaturi", "thumbnail_generation_job_description": "Generează miniaturi mari, mici și estompate pentru fiecare resursă, precum și miniaturi pentru fiecare persoană", "transcoding_acceleration_api": "API de accelerare", "transcoding_acceleration_api_description": "API-ul care va interacționa cu dispozitivul tău pentru a accelera transcodarea. Această setare este 'cel mai bun efort': va reveni la transcodarea software în caz de eșec. VP9 poate funcționa sau nu, în funcție de hardware-ul tău.", @@ -313,7 +325,7 @@ "transcoding_disabled_description": "Nu transcodifică niciun videoclip; acest lucru poate afecta redarea pe anumite dispozitive", "transcoding_encoding_options": "Opțiuni codificare", "transcoding_encoding_options_description": "Setează codecuri , calitatea, rezoluția și alte opțiuni pentru videoclipuri codificare", - "transcoding_hardware_acceleration": "Accelerare Hardware", + "transcoding_hardware_acceleration": "Accelerare hardware", "transcoding_hardware_acceleration_description": "Experimental: transcodare mai rapidă, dar poate reduce calitatea la aceeași rată de biți", "transcoding_hardware_decoding": "Decodare hardware", "transcoding_hardware_decoding_setting_description": "Se aplică doar pentru NVENC, QSV și RKMPP. Activează accelerarea completă în loc de doar accelerarea codificării. S-ar putea să nu funcționeze pentru toate videoclipurile.", @@ -333,7 +345,7 @@ "transcoding_reference_frames": "Cadre de referință", "transcoding_reference_frames_description": "Numărul de cadre de referință atunci când se comprimă un cadru dat. Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. 0 setează această valoare automat.", "transcoding_required_description": "Numai videoclipuri care nu sunt într-un format acceptat", - "transcoding_settings": "Setări de Transcodare Video", + "transcoding_settings": "Setări de transcodare video", "transcoding_settings_description": "Gestionează care videoclipuri să transcodam și cum să le procesam", "transcoding_target_resolution": "Rezoluția țintă", "transcoding_target_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru codare, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.", @@ -354,6 +366,9 @@ "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ", "trash_settings": "Setǎri Coș de Gunoi", "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi", + "unlink_all_oauth_accounts": "Deconectează toate conturile OAuth", + "unlink_all_oauth_accounts_description": "Nu uita să deconectezi toate conturile OAuth înainte de a migra la un nou furnizor.", + "unlink_all_oauth_accounts_prompt": "Ești sigur că vrei să deconectezi toate conturile OAuth? Aceasta va reseta ID-ul OAuth pentru fiecare utilizator și nu poate fi anulată.", "user_cleanup_job": "Curățare utilizator", "user_delete_delay": "Contul și resursele utilizatorului {user} vor fi programate pentru ștergere permanentă în {delay, plural, one {# zi} other {# zile}}.", "user_delete_delay_settings": "Întârziere la ștergere", @@ -361,27 +376,25 @@ "user_delete_immediately": "Contul și resursele utilizatorului {user} vor fi puse în coadă pentru ștergere permanentă imediat.", "user_delete_immediately_checkbox": "Pune utilizatorul și resursele în coadă pentru ștergere imediată", "user_details": "Detalii utilizator", - "user_management": "Gestionarea Utilizatorilor", + "user_management": "Gestionarea utilizatorilor", "user_password_has_been_reset": "Parola utilizatorului a fost resetată:", "user_password_reset_description": "Vă rugăm să furnizați utilizatorului parola temporară și să îi informați că va trebui să o schimbe la următoarea autentificare.", "user_restore_description": "Contul utilizatorului {user} va fi restaurat.", "user_restore_scheduled_removal": "Restaurare utilizator - ștergere programată pe {date, date, long}", - "user_settings": "Setǎri Utilizator", + "user_settings": "Setǎri utilizator", "user_settings_description": "Gestioneazǎ setǎrile utilizatorului", "user_successfully_removed": "Utilizatorul {email} a fost eliminat cu succes.", "version_check_enabled_description": "Activează verificarea versiunii", "version_check_implications": "Funcția de verificare a versiunii se bazează pe comunicarea periodică cu github.com", - "version_check_settings": "Verificare Versiune", + "version_check_settings": "Verificare versiune", "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni", "video_conversion_job": "Transcodați videoclipuri", "video_conversion_job_description": "Transcodați videoclipurile pentru o compatibilitate mai mare cu browserele și dispozitivele" }, - "admin_email": "E-mail Administrator", - "admin_password": "Parolă Administrator", + "admin_email": "E-mail administrator", + "admin_password": "Parolă administrator", "administration": "Administrare", "advanced": "Avansat", - "advanced_settings_beta_timeline_subtitle": "Încearcă noua experiență în aplicație", - "advanced_settings_beta_timeline_title": "Cronologie beta", "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: {level}", @@ -389,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Preferă fotografii la distanță", "advanced_settings_proxy_headers_subtitle": "Definește antetele proxy pe care Immich ar trebui să le trimită cu fiecare solicitare de rețea", "advanced_settings_proxy_headers_title": "Antete Proxy", + "advanced_settings_readonly_mode_subtitle": "Activează modul doar-citire, în care fotografiile pot fi doar vizualizate, iar acțiuni precum selectarea mai multor imagini, partajarea, redarea pe alt dispozitiv sau ștergerea sunt dezactivate. Activează/Dezactivează modul doar-citire din avatarul utilizatorului de pe ecranul principal", + "advanced_settings_readonly_mode_title": "Mod doar-citire", "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", @@ -416,6 +431,7 @@ "album_remove_user_confirmation": "Ești sigur că dorești eliminarea {user}?", "album_search_not_found": "Nu s-au găsit albume care să corespundă căutării dumneavoastră", "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_summary": "Rezumat album", "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}", @@ -454,6 +470,7 @@ "app_bar_signout_dialog_title": "Deconectare", "app_settings": "Setări Aplicație", "appears_in": "Apare în", + "apply_count": "Aplică ({count, number})", "archive": "Arhivă", "archive_action_prompt": "{count} adăugate la Arhivă", "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia", @@ -472,7 +489,7 @@ "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": "Calculare amprentă digitală", + "asset_hashing": "Calculare amprentă digitală…", "asset_list_group_by_sub_title": "Grupare după", "asset_list_layout_settings_dynamic_layout_title": "Aspect dinamic", "asset_list_layout_settings_group_automatically": "Automat", @@ -486,6 +503,8 @@ "asset_restored_successfully": "Date restaurate cu succes", "asset_skipped": "Sărit", "asset_skipped_in_trash": "În coșul de gunoi", + "asset_trashed": "Resursă ștearsă", + "asset_troubleshoot": "Depanare resursă", "asset_uploaded": "Încărcat", "asset_uploading": "Se incarcă…", "asset_viewer_settings_subtitle": "Gestionați setările de vizualizare a galeriei", @@ -493,7 +512,9 @@ "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_albums_count": "Au fost adăugate {assetTotal, plural, one {# element} other {# elemente}} la {albumTotal, plural, one {# album} other {# albume}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} nu pot fi adăugate în album", + "assets_cannot_be_added_to_albums": "{count, plural, one {Elementul} other {Elementele}} nu poate fi adăugat la niciunul dintre albume", "assets_count": "{count, plural, one {# resursă} other {# resurse}}", "assets_deleted_permanently": "{count} poză/poze ștearsă/șterse permanent", "assets_deleted_permanently_from_server": "{count} poză/poze ștearsă/șterse permanent din serverul Immich", @@ -510,14 +531,17 @@ "assets_trashed_count": "Mutat în coșul de gunoi {count, plural, one {# resursă} other {# resurse}}", "assets_trashed_from_server": "{count} resursă(e) eliminate de pe serverul Immich", "assets_were_part_of_album_count": "{count, plural, one {Resursa era} other {Resursele erau}} deja parte din album", + "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} deja parte din albume", "authorized_devices": "Dispozitive Autorizate", "automatic_endpoint_switching_subtitle": "Conectează-te local prin rețeaua Wi‐Fi configurată când este valabilă și prin rețele alternative în caz contrar", "automatic_endpoint_switching_title": "Alternare URL automată", "autoplay_slideshow": "Derulare slideshow automat", "back": "Înapoi", "back_close_deselect": "Înapoi, închidere sau deselectare", + "background_backup_running_error": "Procesul de backup în fundal este activ, nu se poate porni backup manual", "background_location_permission": "Permisiune locație în fundal", "background_location_permission_content": "Pentru a putea schimba rețeaua activă în fundal, Immich are nevoie de acces *permanent* la locația precisă pentru a citi numele rețelei Wi-Fi", + "background_options": "Opțiuni de fundal", "backup": "Backup", "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", @@ -525,6 +549,7 @@ "backup_album_selection_page_select_albums": "Selectează albume", "backup_album_selection_page_selection_info": "Informații selecție", "backup_album_selection_page_total_assets": "Total resurse unice", + "backup_albums_sync": "Sincronizarea albumelor de backup", "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…", @@ -564,7 +589,7 @@ "backup_controller_page_remainder": "Rămas(e)", "backup_controller_page_remainder_sub": "Fotografii și videoclipuri din selecție rămase pentru backup", "backup_controller_page_server_storage": "Stocare server", - "backup_controller_page_start_backup": "Începe backup", + "backup_controller_page_start_backup": "Începe copia de rezervă", "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": "{used} din {total} folosit", @@ -579,12 +604,11 @@ "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": "Opțiuni Backup", + "backup_options": "Opțiuni copie de rezervă", + "backup_options_page_title": "Opțiuni copie de rezervă", "backup_setting_subtitle": "Schimbă opțiuni pentru backup în prim-plan și în fundal", "backup_settings_subtitle": "Gestionați setările de încărcare", "backward": "În sens invers", - "beta_sync": "Starea sincronizării Beta", - "beta_sync_subtitle": "Gestionați noul sistem de sincronizare", "biometric_auth_enabled": "Autentificare biometrică activată", "biometric_locked_out": "Sunteți blocați de la autentificare biometrică", "biometric_no_options": "Nu sunt disponibile opțiuni biometrice", @@ -592,7 +616,7 @@ "birthdate_saved": "Data nașterii salvată cu succes", "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vârsta acestei persoane la momentul realizării fotografiei.", "blurred_background": "Fundal neclar", - "bugs_and_feature_requests": "Erori și Solicitări de Caracteristici", + "bugs_and_feature_requests": "Erori și solicitări de caracteristici", "build": "Versiunea", "build_image": "Versiune Imagine", "bulk_delete_duplicates_confirmation": "Ești sigur că vrei să ștergi în masă {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va șterge permanent toate celelalte duplicate. Nu poți anula această acțiune!", @@ -642,6 +666,8 @@ "change_pin_code": "Schimbă codul PIN", "change_your_password": "Schimbă-ți parola", "changed_visibility_successfully": "Schimbare vizibilitate cu succes", + "charging": "Încărcare", + "charging_requirement_mobile_backup": "Pentru copia de rezervă în fundal, dispozitivul trebuie să fie în curs de încărcare", "check_corrupt_asset_backup": "Verifică copii de rezervă a resurselor corupte", "check_corrupt_asset_backup_button": "Efectuează verificarea", "check_corrupt_asset_backup_description": "Rulează această verificare doar prin Wi-Fi și doar după ce toate resursele au fost salvate în copia de rezerva. Procedura poate dura câteva minute.", @@ -693,7 +719,7 @@ "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_edit_time": "Editează data și ora", "control_bottom_app_bar_share_link": "Partajează linkul", "control_bottom_app_bar_share_to": "Distribuire către", "control_bottom_app_bar_trash_from_immich": "Mută în coș", @@ -728,6 +754,7 @@ "create_user": "Creează utilizator", "created": "Creat", "created_at": "Creat", + "creating_linked_albums": "Crearea albumelor cu link...", "crop": "Decupează", "curated_object_page_title": "Obiecte", "current_device": "Dispozitiv curent", @@ -747,6 +774,7 @@ "date_of_birth_saved": "Data nașterii salvată cu succes", "date_range": "Interval de date", "day": "Zi", + "days": "Zile", "deduplicate_all": "Deduplicați Toate", "deduplication_criteria_1": "Marimea imagini în octeți", "deduplication_criteria_2": "Numărul de date EXIF", @@ -831,10 +859,12 @@ "edit": "Editare", "edit_album": "Editare album", "edit_avatar": "Editare avatar", - "edit_birthday": "Editează ziua de naștere", + "edit_birthday": "Modifică ziua de naștere", "edit_date": "Editare dată", "edit_date_and_time": "Editare dată și oră", "edit_date_and_time_action_prompt": "{count} data și ora modificării", + "edit_date_and_time_by_offset": "Schimbă data prin decalaj", + "edit_date_and_time_by_offset_interval": "Noul interval de date: {from} - {to}", "edit_description": "Editează descrierea", "edit_description_prompt": "Vă rugăm să selectați o descriere nouă:", "edit_exclusion_pattern": "Editarea modelului de excludere", @@ -844,7 +874,7 @@ "edit_key": "Tastă de editare", "edit_link": "Editare link", "edit_location": "Editare locație", - "edit_location_action_prompt": "{count} locație(i) editată(e)", + "edit_location_action_prompt": "{count} locație(i) modificată(e)", "edit_location_dialog_title": "Locație", "edit_name": "Editare nume", "edit_people": "Editare persoane", @@ -874,7 +904,9 @@ "error": "Eroare", "error_change_sort_album": "Nu s-a putut modifica ordinea de sortare a albumului", "error_delete_face": "Eroare la ștergerea feței din activ", + "error_getting_places": "Eroare la obținerea locațiilor", "error_loading_image": "Eroare la încărcarea imaginii", + "error_loading_partners": "Eroare la încărcarea partenerilor: {error}", "error_saving_image": "Eroare: {error}", "error_tag_face_bounding_box": "Eroare la etichetarea feței - nu se pot obține coordonatele casetei de delimitare", "error_title": "Eroare - ceva nu a mers", @@ -907,6 +939,7 @@ "failed_to_load_notifications": "Nu s-au putut încărca notificările", "failed_to_load_people": "Eșec la încărcarea persoanelor", "failed_to_remove_product_key": "Eșec la eliminarea cheii de produs", + "failed_to_reset_pin_code": "Nu s-a reușit resetarea codului PIN", "failed_to_stack_assets": "Eșec la combinarea resurselor", "failed_to_unstack_assets": "Eșec la desfășurarea resurselor", "failed_to_update_notification_status": "Nu s-a putut actualiza starea notificării", @@ -915,6 +948,7 @@ "paths_validation_failed": "{paths, plural, one {# cale} other {# căi}} nu a trecut validarea", "profile_picture_transparent_pixels": "Pozele de profil nu pot avea pixeli transparenți. Te rugăm să mărești imaginea și/sau să o muți.", "quota_higher_than_disk_size": "Ați stabilit o valoare a spațiului de stocare mai mare decât dimensiunea discului", + "something_went_wrong": "Ceva nu a mers bine", "unable_to_add_album_users": "Imposibil de adăugat utilizatori în album", "unable_to_add_assets_to_shared_link": "Imposibil de adăugat resurse la link-ul partajat", "unable_to_add_comment": "Imposibil de adăugat comentariu", @@ -1022,7 +1056,7 @@ "export_database_description": "Exportați baza de date SQLite", "extension": "Extensie", "external": "Extern", - "external_libraries": "Biblioteci Externe", + "external_libraries": "Biblioteci externe", "external_network": "Rețea externă", "external_network_sheet_info": "Când nu se află în rețeaua Wi-Fi preferată, aplicația se va conecta la server prin prima dintre adresele URL de mai jos pe care o poate accesa, începând de sus în jos", "face_unassigned": "Nealocat", @@ -1037,6 +1071,7 @@ "favorites_page_no_favorites": "Nu au fost găsite resurse favorite", "feature_photo_updated": "Fotografie caracteristică actualizată", "features": "Caracteristici", + "features_in_development": "Funcții în dezvoltare", "features_setting_description": "Gestionați funcțiile aplicației", "file_name": "Nume de fișier", "file_name_or_extension": "Numele sau extensia fișierului", @@ -1046,21 +1081,26 @@ "filter_people": "Filtrați persoanele", "filter_places": "Filtrează locurile", "find_them_fast": "Găsiți-le rapid prin căutare după nume", + "first": "Primul", "fix_incorrect_match": "Remediați potrivirea incorectă", "folder": "Dosar", "folder_not_found": "Dosar negăsit", "folders": "Foldere", "folders_feature_description": "Răsfoire în conținutul folderului pentru fotografiile și videoclipurile din sistemul de fișiere", + "forgot_pin_code_question": "Ai uitat codul PIN?", "forward": "Redirecționare", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Această funcție încarcă resurse externe de la Google pentru a funcționa.", "general": "General", + "geolocation_instruction_location": "Apasă pe o resursă cu coordonate GPS pentru a folosi locația sa, sau selectează direct o locație de pe hartă", "get_help": "Obțineți Ajutor", "get_wifiname_error": "Nu s-a putut obține numele rețelei Wi-Fi. Asigurați-vă că ați acordat permisiunile necesare și că sunteți conectat la o rețea Wi-Fi", "getting_started": "Noțiuni de Bază", "go_back": "Întoarcere", "go_to_folder": "Accesați folderul", "go_to_search": "Spre căutare", + "gps": "GPS", + "gps_missing": "Fără GPS", "grant_permission": "Acordați permisiunea", "group_albums_by": "Grupați albume de...", "group_country": "Grupare după țară", @@ -1071,6 +1111,9 @@ "haptic_feedback_switch": "Activează feedback-ul haptic", "haptic_feedback_title": "Feedback haptic", "has_quota": "Are spațiu de stocare", + "hash_asset": "Hash-ul resursei", + "hashed_assets": "Resurse hashed", + "hashing": "Generare hash", "header_settings_add_header_tip": "Adăugați antet", "header_settings_field_validator_msg": "Valoarea nu poate fi goală", "header_settings_header_name_input": "Numele antetului", @@ -1097,11 +1140,12 @@ "home_page_favorite_err_partner": "Momentan nu se pot adăuga fișierele partenerului la favorite, omitere", "home_page_first_time_notice": "Dacă este prima dată când utilizezi aplicația, te rugăm să te asiguri că alegi unul sau mai multe albume de backup, astfel încât cronologia să poată fi populată cu fotografiile și videoclipurile din aceste albume", "home_page_locked_error_local": "Nu se pot muta resursele locale în folderul blocat, se omit", - "home_page_locked_error_partner": "Nu se pot muta materialele partenerului în folderul blocat, se omit.", + "home_page_locked_error_partner": "Nu se pot muta resursele partenerului în folderul blocat, se omit.", "home_page_share_err_local": "Nu se pot distribui fișiere locale prin link, omitere", "home_page_upload_err_limit": "Se pot încărca maxim 30 de resurse odată, omitere", "host": "Gazdă", "hour": "Oră", + "hours": "Ore", "id": "ID", "idle": "Inactiv", "ignore_icloud_photos": "Ignoră fotografiile din iCloud", @@ -1161,10 +1205,13 @@ "language_no_results_title": "Nu au fost găsite limbi", "language_search_hint": "Căutați limbi...", "language_setting_description": "Selectați limba preferată", + "large_files": "Fișiere mari", + "last": "Ultimul", "last_seen": "Văzut ultima dată", "latest_version": "Ultima Versiune", "latitude": "Latitudine", "leave": "Părăsiți", + "leave_album": "Părăsește albumul", "lens_model": "Model obiectiv", "let_others_respond": "Permite altora să răspundă", "level": "Nivel", @@ -1178,6 +1225,7 @@ "library_page_sort_title": "Titlu album", "licenses": "Licențe", "light": "Lumină", + "like": "Îmi place", "like_deleted": "Preferat șters", "link_motion_video": "Link video în mișcare", "link_to_oauth": "Link către OAuth", @@ -1188,6 +1236,7 @@ "local": "Local", "local_asset_cast_failed": "Nu se poate converti un element care nu este încărcat pe server", "local_assets": "Asset-uri locale", + "local_media_summary": "Rezumatul fișierelor media locale", "local_network": "Rețea locală", "local_network_sheet_info": "Aplicația se va conecta la server prin intermediul acestei adrese URL atunci când utilizează rețeaua Wi-Fi specificată", "location_permission": "Permisiunea de locație", @@ -1199,6 +1248,7 @@ "location_picker_longitude_hint": "Introdu longitudinea aici", "lock": "Blocare", "locked_folder": "Dosar blocat", + "log_detail_title": "Detalii jurnal", "log_out": "Deconectare", "log_out_all_devices": "Deconectați-vă de la toate dispozitivele", "logged_in_as": "Conectat ca {user}", @@ -1229,6 +1279,7 @@ "login_password_changed_success": "Parola a fost actualizată cu succes", "logout_all_device_confirmation": "Sigur doriți să deconectați toate dispozitivele?", "logout_this_device_confirmation": "Sigur doriți să deconectați acest dispozitiv?", + "logs": "Jurnale", "longitude": "Longitudine", "look": "Examinare", "loop_videos": "Buclă videoclipuri", @@ -1236,6 +1287,7 @@ "main_branch_warning": "Utilizați o versiune de dezvoltare; vă recomandăm insistent să utilizați o versiune de lansare!", "main_menu": "Meniu principal", "make": "Face", + "manage_geolocation": "Gestionați locația", "manage_shared_links": "Administrați link-urile distribuite", "manage_sharing_with_partners": "Gestionați partajarea cu partenerii", "manage_the_app_settings": "Gestionați setările aplicației", @@ -1244,7 +1296,7 @@ "manage_your_devices": "Gestionați-vă dispozitivele conectate", "manage_your_oauth_connection": "Gestionați-vă conexiunea OAuth", "map": "Hartă", - "map_assets_in_bounds": "{count, plural, one {# poză} other {# poze}}", + "map_assets_in_bounds": "{count, plural, =0 {Nu există fotografii în această zonă} one {# fotografie} other {# 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", @@ -1270,6 +1322,7 @@ "mark_as_read": "Marchează ca citit", "marked_all_as_read": "Marcate toate ca citite", "matches": "Corespunde", + "matching_assets": "Resurse similare", "media_type": "Tip media", "memories": "Amintiri", "memories_all_caught_up": "Sunteți la zi", @@ -1287,7 +1340,8 @@ "merge_people_successfully": "Persoane îmbinate cu succes", "merged_people_count": "Imbinate {count, plural, one {# persoană} other {# persoane}}", "minimize": "Minimizare", - "minute": "Minute", + "minute": "Minut", + "minutes": "Minute", "missing": "Lipsă", "model": "Model", "month": "Lună", @@ -1307,6 +1361,10 @@ "my_albums": "Albumele mele", "name": "Nume", "name_or_nickname": "Nume sau poreclǎ", + "network_requirement_photos_upload": "Utilizați datele mobile pentru a face copii de rezervă ale fotografiilor", + "network_requirement_videos_upload": "Utilizați datele mobile pentru a face copii de rezervă ale videoclipurilor", + "network_requirements": "Cerințe privind rețeaua", + "network_requirements_updated": "Cerințele rețelei s-au modificat, resetarea cozii copiei de rezervă", "networking_settings": "Rețele", "networking_subtitle": "Gestionați setările endpoint-ului serverului", "never": "Niciodată", @@ -1316,6 +1374,7 @@ "new_person": "Persoanǎ nouǎ", "new_pin_code": "Cod PIN nou", "new_pin_code_subtitle": "Aceasta este prima dată când accesați folderul blocat. Creați un cod PIN pentru a accesa în siguranță această pagină", + "new_timeline": "Noua cronologie", "new_user_created": "Utilizator nou creat", "new_version_available": "VERSIUNE NOUĂ DISPONIBILĂ", "newest_first": "Cel mai nou primul", @@ -1329,20 +1388,25 @@ "no_assets_message": "CLICK PENTRU A ÎNCĂRCA PRIMA TA FOTOGRAFIE", "no_assets_to_show": "Nicio resursă de afișat", "no_cast_devices_found": "Nu s-au găsit dispozitive de difuzare", + "no_checksum_local": "Nu există checksum – nu se pot prelua resursele locale", + "no_checksum_remote": "Nu există checksum – nu se pot prelua resursele la distanță", "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.", "no_favorites_message": "Adăugați favorite pentru a găsi rapid cele mai bune fotografii și videoclipuri", "no_libraries_message": "Creați o bibliotecă externă pentru a vă vizualiza fotografiile și videoclipurile", + "no_local_assets_found": "Nicio resursă locală găsită cu acest checksum", "no_locked_photos_message": "Fotografiile și videoclipurile din folderul blocat sunt ascunse și nu vor apărea atunci când răsfoiți sau căutați în bibliotecă.", "no_name": "Fără Nume", "no_notifications": "Nicio notificare", "no_people_found": "Nu au fost găsite persoane potrivite căutării", "no_places": "Nu există locuri", + "no_remote_assets_found": "Nicio resursă de la distanță găsită cu acest checksum", "no_results": "Fără rezultate", "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", "no_uploads_in_progress": "Nicio încărcare în curs", + "not_available": "N/A", "not_in_any_album": "Nu există în niciun album", "not_selected": "Neselectat", "note_apply_storage_label_to_previously_uploaded assets": "Notă: Pentru a aplica eticheta de stocare la resursele încărcate anterior, rulați", @@ -1358,6 +1422,7 @@ "oauth": "OAuth", "official_immich_resources": "Resurse Oficiale Immich", "offline": "Offline", + "offset": "Decalaj", "ok": "Bine", "oldest_first": "Cel mai vechi mai întâi", "on_this_device": "Pe acest dispozitiv", @@ -1376,6 +1441,8 @@ "open_the_search_filters": "Deschideți filtrele de căutare", "options": "Opțiuni", "or": "sau", + "organize_into_albums": "Organizați în albume", + "organize_into_albums_description": "Pune fotografiile existente în albume folosind setările curente de sincronizare", "organize_your_library": "Organizează-ți biblioteca", "original": "original", "other": "Alte", @@ -1435,6 +1502,9 @@ "permission_onboarding_permission_limited": "Permisiune limitată. Pentru a permite Immich să facă copii de siguranță și să gestioneze întreaga colecție de galerii, acordă permisiuni pentru fotografii și videoclipuri în Setări.", "permission_onboarding_request": "Immich necesită permisiunea de a vizualiza fotografiile și videoclipurile tale.", "person": "Persoanǎ", + "person_age_months": "{months, plural, one {# month} other {# months}} vechime", + "person_age_year_months": "1 year, {months, plural, one {# month} other {# months}} vechime", + "person_age_years": "{years, plural, other {# years}} vechime", "person_birthdate": "Născut pe {date}", "person_hidden": "{name}{hidden, select, true { (ascuns)} other {}}", "photo_shared_all_users": "Se pare că ți-ai partajat fotografiile tuturor utilizatorilor sau că nu ai niciun utilizator căruia să le distribui.", @@ -1458,6 +1528,7 @@ "port": "Port", "preferences_settings_subtitle": "Gestionați preferințele aplicației", "preferences_settings_title": "Preferințe", + "preparing": "Se prepară", "preset": "Presetat", "preview": "Previzualizare", "previous": "Anterior", @@ -1474,6 +1545,7 @@ "profile_drawer_client_out_of_date_minor": "Aplicația nu folosește ultima versiune. Te rugăm să actualizezi 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_readonly_mode": "Mod doar citire activat. Ține apăsat pe pictograma avatarului utilizatorului pentru a ieși.", "profile_drawer_server_out_of_date_major": "Server-ul nu folosește ultima versiune. Te rugăm să actualizezi 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}", @@ -1512,6 +1584,7 @@ "purchase_server_description_2": "Statutul de suporter", "purchase_server_title": "Server", "purchase_settings_server_activated": "Cheia de produs a serverului este gestionată de administrator", + "query_asset_id": "Interoghează ID-ul resursei", "queue_status": "Se pun în coadă {count}/{total}", "rating": "Evaluare cu stele", "rating_clear": "Anulați evaluarea", @@ -1519,6 +1592,9 @@ "rating_description": "Afișați evaluarea EXIF în panoul de informații", "reaction_options": "Opțiuni de reacție", "read_changelog": "Citiți Jurnalul de Modificări", + "readonly_mode_disabled": "Modul doar citire dezactivat", + "readonly_mode_enabled": "Modul doar citire activat", + "ready_for_upload": "Pregătit pentru încărcare", "reassign": "Reatribuiți", "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", @@ -1543,6 +1619,7 @@ "regenerating_thumbnails": "Se regenerează miniaturile", "remote": "De la distanță", "remote_assets": "Elemente la distanță", + "remote_media_summary": "Rezumat media de la distanță", "remove": "Eliminați", "remove_assets_album_confirmation": "Sigur doriți să eliminați {count, plural, one {# resursă} other {# resurse}} din album?", "remove_assets_shared_link_confirmation": "Sigur doriți să eliminați {count, plural, one {# resursă} other {# resurse}} din acest link comun?", @@ -1580,6 +1657,9 @@ "reset_password": "Resetare parolă", "reset_people_visibility": "Resetați vizibilitatea persoanelor", "reset_pin_code": "Resetare cod PIN", + "reset_pin_code_description": "Dacă ți-ai uitat codul PIN, poți contacta administratorul serverului pentru a-l reseta", + "reset_pin_code_success": "Codul PIN a fost resetat cu succes", + "reset_pin_code_with_password": "Puteți reseta oricând codul PIN cu ajutorul parolei", "reset_sqlite": "Resetare bază de date SQLite", "reset_sqlite_confirmation": "Sigur doriți să resetați baza de date SQLite? Va trebui să vă deconectați și să vă conectați din nou pentru a resincroniza datele", "reset_sqlite_success": "Resetarea cu succes a bazei de date SQLite", @@ -1592,8 +1672,10 @@ "restore_user": "Restabiliți utilizatorul", "restored_asset": "Resursă restaurată", "resume": "Reluare", + "resume_paused_jobs": "Reluați {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Reîncercați încărcarea", "review_duplicates": "Examinați duplicatele", + "review_large_files": "Revizuirea fișierelor mari", "role": "Rol", "role_editor": "Editor", "role_viewer": "Vizualizator", @@ -1605,7 +1687,7 @@ "saved_settings": "Setări salvate", "say_something": "Spuneți ceva", "scaffold_body_error_occurred": "A apărut o eroare", - "scan_all_libraries": "Scanați Toate Bibliotecile", + "scan_all_libraries": "Scanați toate bibliotecile", "scan_library": "Scanare", "scan_settings": "Setări Scanare", "scanning_for_album": "Se scanează după album...", @@ -1684,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Creare album eșuată", "selected": "Selectat", "selected_count": "{count, plural, other {# selectat}}", + "selected_gps_coordinates": "Coordonate GPS selectate", "send_message": "Trimiteți mesaj", "send_welcome_email": "Trimiteți email de bun venit", "server_endpoint": "Endpoint server", @@ -1692,7 +1775,7 @@ "server_offline": "Serverul este offline", "server_online": "Server online", "server_privacy": "Confidențialitatea serverului", - "server_stats": "Statistici Server", + "server_stats": "Statistici server", "server_version": "Versiune Server", "set": "Setați", "set_as_album_cover": "Setați ca și copertă a albumului", @@ -1751,6 +1834,7 @@ "shared_link_clipboard_copied_massage": "Copiat în clipboard", "shared_link_clipboard_text": "Link: {link}\nParolă: {password}", "shared_link_create_error": "Eroare în timpul creării linkului de distribuire", + "shared_link_custom_url_description": "Accesează acest link partajat cu un URL personalizat", "shared_link_edit_description_hint": "Introdu descrierea distribuirii", "shared_link_edit_expire_after_option_day": "1 zi", "shared_link_edit_expire_after_option_days": "{count} zile", @@ -1776,6 +1860,7 @@ "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Administrează link-urile distribuite", "shared_link_options": "Opțiuni de link partajat", + "shared_link_password_description": "Solicită o parolă pentru a accesa acest 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.}}", @@ -1810,6 +1895,7 @@ "show_slideshow_transition": "Afișați tranziția de prezentare", "show_supporter_badge": "Insigna suporterului", "show_supporter_badge_description": "Arată o insignă de suporter", + "show_text_search_menu": "Afișează meniul de căutare text", "shuffle": "Amestecați", "sidebar": "Bara laterală", "sidebar_display_description": "Afișați un link către vizualizare în bara laterală", @@ -1825,6 +1911,7 @@ "sort_created": "Data creării", "sort_items": "Numărul de articole", "sort_modified": "Data modificării", + "sort_newest": "Cea mai nouă fotografie", "sort_oldest": "Cea mai veche fotografie", "sort_people_by_similarity": "Sortează oameni după asemanare", "sort_recent": "Cea mai recentă fotografie", @@ -1839,6 +1926,7 @@ "stacktrace": "Urmă stivă", "start": "Început", "start_date": "Data de începere", + "start_date_before_end_date": "Data de început trebuie să fie înainte de data de sfârșit", "state": "Situaţie", "status": "Stare", "stop_casting": "Opriți difuzarea", @@ -1863,6 +1951,8 @@ "sync_albums_manual_subtitle": "Sincronizează toate videoclipurile și fotografiile încărcate cu albumele de rezervă selectate", "sync_local": "Sincronizare locală", "sync_remote": "Sincronizare la distanță", + "sync_status": "Status-ul sincronizării", + "sync_status_subtitle": "Vizualizează și gestionează sistemul de sincronizare", "sync_upload_album_setting_subtitle": "Creează și încarcă fotografiile și videoclipurile tale în albumele selectate de pe Immich", "tag": "Etichetă", "tag_assets": "Eticheta resurselor", @@ -1900,7 +1990,9 @@ "to_change_password": "Schimbaţi parola", "to_favorite": "Favorit", "to_login": "Conectare", + "to_multi_select": "pentru selecție multiplă", "to_parent": "Du-te la părinte", + "to_select": "a selecta", "to_trash": "Coș de gunoi", "toggle_settings": "Activați setările", "total": "Total", @@ -1920,6 +2012,7 @@ "trash_page_select_assets_btn": "Selectează resurse", "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}}.", + "troubleshoot": "Depanați", "type": "Tip", "unable_to_change_pin_code": "Nu se poate schimba codul PIN", "unable_to_setup_pin_code": "Nu se poate configura codul PIN", @@ -1946,10 +2039,11 @@ "unselect_all_duplicates": "Deselectați toate duplicatele", "unselect_all_in": "Deselectați toate din {group}", "unstack": "Dezasamblați", - "unstack_action_prompt": "{count} unstacked", + "unstack_action_prompt": "{count} neîmpachetate", "unstacked_assets_count": "Nestivuit {count, plural, one {# resursă} other {# resurse}}", "untagged": "Neetichetat", "up_next": "Mai departe", + "update_location_action_prompt": "Actualizează locația pentru {count} resurse selectate cu:", "updated_at": "Actualizat", "updated_password": "Parolă actualizată", "upload": "Încărcați", @@ -2016,13 +2110,14 @@ "view_next_asset": "Vizualizați următoarea resursă", "view_previous_asset": "Vizualizați resursa anterioară", "view_qr_code": "Vezi cod QR", + "view_similar_photos": "Vizualizați poze similare", "view_stack": "Vizualizați Stiva", "view_user": "Vizualizare utilizator", "viewer_remove_from_stack": "Șterge din grup", "viewer_stack_use_as_main_asset": "Folosește ca resursă principală", "viewer_unstack": "Anulează grup", "visibility_changed": "Vizibilitatea schimbată pentru {count, plural, one {# persoană} other {# persoane}}", - "waiting": "Așteptați", + "waiting": "În așteptare", "warning": "Avertisment", "week": "Sǎptǎmânǎ", "welcome": "Bun venit", @@ -2034,5 +2129,6 @@ "yes": "Da", "you_dont_have_any_shared_links": "Nu aveți linkuri partajate", "your_wifi_name": "Numele rețelei tale WiFi", - "zoom_image": "Măriți Imaginea" + "zoom_image": "Măriți Imaginea", + "zoom_to_bounds": "Mărește la margini" } diff --git a/i18n/ru.json b/i18n/ru.json index 4e3fdcf3ec..f17b5ce364 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -7,7 +7,7 @@ "action_common_update": "Обновить", "actions": "Действия", "active": "Выполняется", - "activity": "Активность", + "activity": "Действия", "activity_changed": "Активность {enabled, select, true {включена} other {отключена}}", "add": "Добавить", "add_a_description": "Добавить описание", @@ -26,8 +26,9 @@ "add_tag": "Добавить тег", "add_to": "Добавить в…", "add_to_album": "Добавить в альбом", - "add_to_album_bottom_sheet_added": "Добавлено в {album}", - "add_to_album_bottom_sheet_already_exists": "Уже в {album}", + "add_to_album_bottom_sheet_added": "Добавлено в альбом {album}", + "add_to_album_bottom_sheet_already_exists": "Уже в альбоме {album}", + "add_to_album_bottom_sheet_some_local_assets": "Некоторые объекты не добавлены в альбом, поскольку еще не загружены на сервер", "add_to_album_toggle": "Переключить выделение для альбома {album}", "add_to_albums": "Добавить в альбомы", "add_to_albums_count": "Добавить в альбомы ({count})", @@ -35,11 +36,11 @@ "add_url": "Добавить URL", "added_to_archive": "Добавлено в архив", "added_to_favorites": "Добавлено в избранное", - "added_to_favorites_count": "Добавлено{count, number} в избранное", + "added_to_favorites_count": "{count, plural, one {# объект добавлен} many {# объектов добавлено} other {# объекта добавлено}} в избранное", "admin": { "add_exclusion_pattern_description": "Добавьте шаблоны исключений. Поддерживаются символы подстановки *, ** и ?. Чтобы игнорировать все файлы в любом каталоге с именем \"Raw\", укажите \"**/Raw/**\". Чтобы игнорировать все файлы, заканчивающиеся на \".tif\", используйте \"**/*.tif\". Чтобы игнорировать путь целиком, укажите \"/path/to/ignore/**\".", "admin_user": "Администратор", - "asset_offline_description": "Этот файл внешней библиотеки не был найден на диске и был перемещён в корзину. Если файл был перемещён внутри библиотеки, проверьте временную шкалу, чтобы найти новый соответствующий ресурс. Чтобы восстановить файл, убедитесь, что путь ниже доступен для Immich и выполните сканирование библиотеки.", + "asset_offline_description": "Этот объект из внешней библиотеки не был обнаружен на диске и поэтому перемещён в корзину. Если файл объекта был перемещён внутри библиотеки, проверьте временную шкалу, чтобы найти новый соответствующий объект. Чтобы восстановить файл, убедитесь, что следующий путь доступен для Immich, и выполните сканирование библиотеки.", "authentication_settings": "Настройки аутентификации", "authentication_settings_description": "Управление паролями, OAuth и другими настройками аутентификации", "authentication_settings_disable_all": "Вы уверены, что хотите отключить все методы входа? Вход будет полностью отключен.", @@ -65,9 +66,9 @@ "confirm_reprocess_all_faces": "Вы уверены, что хотите повторно определить все лица? Будут также удалены имена со всех лиц.", "confirm_user_password_reset": "Вы действительно хотите сбросить пароль пользователя {user}?", "confirm_user_pin_code_reset": "Вы действительно хотите сбросить PIN-код пользователя {user}?", - "create_job": "Создать задание", + "create_job": "Создать задачу", "cron_expression": "Расписание (выражение планировщика cron)", - "cron_expression_description": "Частота и время выполнения задания в формате планировщика cron. Воспользуйтесь при необходимости визуальным редактором Crontab Guru", + "cron_expression_description": "Частота и время выполнения задачи в формате планировщика cron. Воспользуйтесь при необходимости визуальным редактором Crontab Guru", "cron_expression_presets": "Расписание (предустановленные варианты)", "disable_login": "Отключить вход", "duplicate_detection_job_description": "Запускает определение похожих изображений при помощи машинного зрения (зависит от умного поиска)", @@ -77,7 +78,7 @@ "face_detection_description": "Обнаруживает лица на объектах с использованием машинного обучения. Для видео анализируется только миниатюра. Кнопка \"Обновить\" запускает повторную обработку всех объектов. \"Сброс\" — дополнительно удаляет все имеющиеся данные о лицах. \"Отсутствующие\" — ставит в очередь объекты, которые ещё не были обработаны. Обнаруженные лица помещаются в очередь для задачи Распознавание лиц и последующей их привязки к существующим или новым людям.", "facial_recognition_job_description": "Группирует и назначает обнаруженные лица людям. Выполняется после завершения задачи Обнаружение лиц. Кнопка \"Сброс\" (пере)назначает все лица. \"Отсутствующие\" — добавляет в очередь обработки лица, не привязанные к человеку.", "failed_job_command": "Команда {command} не выполнена для задачи: {job}", - "force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и его ресурсов. Это действие невозможно отменить, и файлы не могут быть восстановлены.", + "force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и всех его объектов. Это действие невозможно отменить, файлы не смогут быть восстановлены.", "image_format": "Формат", "image_format_description": "WebP создает файлы меньшего размера, чем JPEG, но кодирует медленнее.", "image_fullsize_description": "Полноразмерное изображение без метаданных, используется при увеличении", @@ -100,11 +101,11 @@ "image_thumbnail_description": "Маленькая миниатюра с удаленными метаданными, используемая при просмотре групп фотографий, таких как основная временная шкала", "image_thumbnail_quality_description": "Качество миниатюр от 1 до 100. Чем выше качество, тем лучше, но при этом создаются файлы большего размера и может снизиться скорость отклика приложения.", "image_thumbnail_title": "Настройки миниатюр", - "job_concurrency": "Параллельная обработка задания - {job}", - "job_created": "Задание создано", + "job_concurrency": "Число параллельных потоков задачи {job}", + "job_created": "Задача создана", "job_not_concurrency_safe": "Эта задача не обеспечивает безопасность параллельности выполнения.", - "job_settings": "Настройки заданий", - "job_settings_description": "Управление параллельной обработкой заданий", + "job_settings": "Настройки задач", + "job_settings_description": "Управление параллельностью выполнения задач", "job_status": "Состояние выполнения задач", "jobs_delayed": "{jobCount, plural, one {# отложена} other {# отложено}}", "jobs_failed": "{jobCount, plural, other {# не удалось выполнить}}", @@ -123,20 +124,27 @@ "logging_enable_description": "Включить ведение журнала", "logging_level_description": "Если включено, выберите желаемый уровень журналирования.", "logging_settings": "Ведение журнала", + "machine_learning_availability_checks": "Проверка доступности", + "machine_learning_availability_checks_description": "Автоматически определять и использовать доступные серверы машинного обучения", + "machine_learning_availability_checks_enabled": "Включить проверку доступности", + "machine_learning_availability_checks_interval": "Интервал проверки", + "machine_learning_availability_checks_interval_description": "Интервал в миллисекундах между проверками", + "machine_learning_availability_checks_timeout": "Тайм-аут запроса", + "machine_learning_availability_checks_timeout_description": "Время ожидания ответа сервера в миллисекундах для определения доступности", "machine_learning_clip_model": "CLIP модель", - "machine_learning_clip_model_description": "Названия моделей CLIP размещены здесь. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Интеллектуальный поиск» для всех изображений.", + "machine_learning_clip_model_description": "Названия доступных CLIP моделей размещены здесь.\nПри изменении модели необходимо заново запустить задачу «Интеллектуальный поиск» для всех объектов.", "machine_learning_duplicate_detection": "Поиск дубликатов", "machine_learning_duplicate_detection_enabled": "Включить обнаружение дубликатов", - "machine_learning_duplicate_detection_enabled_description": "Если этот параметр отключен, абсолютно идентичные файлы всё равно будут удалены из дубликатов.", - "machine_learning_duplicate_detection_setting_description": "Используйте встраивания CLIP для поиска вероятных дубликатов", - "machine_learning_enabled": "Включите машинное обучение", - "machine_learning_enabled_description": "При отключении, все функции ML будут отключены независимо от следующих параметров.", + "machine_learning_duplicate_detection_enabled_description": "Если этот параметр отключён, абсолютно идентичные файлы всё равно не будут загружаться.", + "machine_learning_duplicate_detection_setting_description": "Использование CLIP моделей для выявления возможных дубликатов", + "machine_learning_enabled": "Включить машинное обучение", + "machine_learning_enabled_description": "При выключении будут отключены все функции ML независимо от следующих параметров.", "machine_learning_facial_recognition": "Распознавание лиц", "machine_learning_facial_recognition_description": "Обнаруживать, распознавать и группировать лица на изображениях", "machine_learning_facial_recognition_model": "Модель для распознавания лиц", - "machine_learning_facial_recognition_model_description": "Модели перечислены в порядке убывания размера. Большие модели работают медленнее и используют больше памяти, но дают лучшие результаты. Обратите внимание, что при смене модели необходимо повторно запустить задание распознавания лиц для всех изображений.", + "machine_learning_facial_recognition_model_description": "Модели перечислены в порядке убывания их размера. Большие модели работают медленнее и используют больше памяти, но дают лучшие результаты. При смене модели необходимо повторно запустить задачу распознавания лиц для всех изображений.", "machine_learning_facial_recognition_setting": "Включить функцию распознавания лиц", - "machine_learning_facial_recognition_setting_description": "Если отключить эту функцию, изображения не будут кодироваться для распознавания лиц и не будут заполнять раздел Люди на обзорной странице.", + "machine_learning_facial_recognition_setting_description": "При отключении этой функции изображения не будут кодироваться для распознавания лиц, и не будет заполняться раздел Люди.", "machine_learning_max_detection_distance": "Максимальное различие изображений", "machine_learning_max_detection_distance_description": "Максимальное различие между двумя изображениями, чтобы считать их дубликатами, в диапазоне 0,001-0,1. Более высокие значения позволяют обнаружить больше дубликатов, но могут привести к ложным срабатываниям.", "machine_learning_max_recognition_distance": "Порог распознавания", @@ -146,13 +154,13 @@ "machine_learning_min_recognized_faces": "Минимум распознанных лиц", "machine_learning_min_recognized_faces_description": "Минимальное количество распознанных лиц для создания человека. Увеличение этого параметра делает распознавание лиц более точным, но при этом увеличивается вероятность того, что лицо не будет присвоено человеку.", "machine_learning_settings": "Настройки машинного обучения", - "machine_learning_settings_description": "Управление функциями и настройками машинного обучения", + "machine_learning_settings_description": "Управление функциями и настройками машинного обучения (ML)", "machine_learning_smart_search": "Интеллектуальный поиск", - "machine_learning_smart_search_description": "Семантический поиск изображений с использованием вложений CLIP", + "machine_learning_smart_search_description": "Семантический (контекстный) поиск объектов с использованием CLIP моделей", "machine_learning_smart_search_enabled": "Включить интеллектуальный поиск", - "machine_learning_smart_search_enabled_description": "Если этот параметр отключен, изображения не будут кодироваться для интеллектуального поиска.", + "machine_learning_smart_search_enabled_description": "При отключении этой функции изображения не будут кодироваться для интеллектуального поиска.", "machine_learning_url_description": "URL-адрес сервера машинного обучения. Если указано несколько, запросы будут отправляться по очереди на каждый, пока от одного из них не будет получен успешный ответ. Серверы, которые не отвечают, будут временно игнорироваться до тех пор, пока не станут снова доступны.", - "manage_concurrency": "Управление параллельностью заданий", + "manage_concurrency": "Управление параллельностью", "manage_log_settings": "Управление настройками журнала", "map_dark_style": "Тёмный стиль", "map_enable_description": "Включить функции карты", @@ -170,9 +178,9 @@ "memory_cleanup_job": "Очистка воспоминаний", "memory_generate_job": "Создание воспоминаний", "metadata_extraction_job": "Извлечение метаданных", - "metadata_extraction_job_description": "Извлекает метаданные из каждого файла, такие как местоположение, лица и разрешение", + "metadata_extraction_job_description": "Извлечение метаданных из файлов, таких как местоположение, лица и разрешение", "metadata_faces_import_setting": "Включить импорт лиц", - "metadata_faces_import_setting_description": "Импорт лиц из изображений EXIF-данных и файлов sidecar", + "metadata_faces_import_setting_description": "Импорт лиц из EXIF-данных и файлов sidecar", "metadata_settings": "Настройки метаданных", "metadata_settings_description": "Управление настройками метаданных", "migration_job": "Миграция", @@ -247,7 +255,7 @@ "reset_settings_to_default": "Сброс настроек до значений по умолчанию", "reset_settings_to_recent_saved": "Не сохранённые изменения сброшены к последним сохраненным значениям", "scanning_library": "Сканирование библиотеки", - "search_jobs": "Поиск заданий…", + "search_jobs": "Поиск задач…", "send_welcome_email": "Отправить приветственное письмо", "server_external_domain_settings": "Внешний домен", "server_external_domain_settings_description": "Домен для публичных ссылок, включая http(s)://", @@ -268,8 +276,8 @@ "storage_template_hash_verification_enabled_description": "Включает проверку хеша, не отключайте её, если не уверены в последствиях", "storage_template_migration": "Применение шаблона хранилища", "storage_template_migration_description": "Применяет текущий {template} к ранее загруженным объектам", - "storage_template_migration_info": "Расширения файлов всегда будут сохраняться в нижнем регистре. Изменения в шаблоне будут применяться только к новым ресурсам. Чтобы применить шаблон к ранее загруженным ресурсам, запустите {job}.", - "storage_template_migration_job": "Задание по применению шаблона хранилища", + "storage_template_migration_info": "Расширения файлов всегда будут сохраняться в нижнем регистре. Изменения в шаблоне будут применяться только к новым объектам. Чтобы применить шаблон к ранее загруженным объектам, запустите {job}.", + "storage_template_migration_job": "Задача по применению шаблона хранилища", "storage_template_more_details": "Для получения дополнительной информации об этой функции обратитесь к разделам документации Шаблон хранилища и Структура хранения файлов", "storage_template_onboarding_description_v2": "Если эта функция включена, она автоматически организует файлы на основе заданного пользователем шаблона. Для получения дополнительной информации обратитесь к документации.", "storage_template_path_length": "Примерный предел длины пути: {length, number}/{limit, number}", @@ -364,7 +372,7 @@ "user_cleanup_job": "Очистка пользователя", "user_delete_delay": "Аккаунт и файлы пользователя {user} будут отложены до окончательного удаления через {delay, plural, one {# день} few {# дня} many {# дней} other {# дня}}.", "user_delete_delay_settings": "Отложенное удаление", - "user_delete_delay_settings_description": "Срок в днях, по истечение которого происходит окончательное удаление учетной записи пользователя и его ресурсов. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.", + "user_delete_delay_settings_description": "Срок в днях, по истечении которого происходит окончательное удаление учётной записи пользователя и всех его объектов. Задача по удалению пользователей выполняется в полночь. Изменение этой настройки будет учтено при следующем запуске задачи.", "user_delete_immediately": "Аккаунт и файлы пользователя {user} будут немедленно поставлены в очередь для окончательного удаления.", "user_delete_immediately_checkbox": "Поместить пользователя и его файлы в очередь для немедленного удаления", "user_details": "Данные пользователя", @@ -387,27 +395,27 @@ "admin_password": "Пароль администратора", "administration": "Управление сервером", "advanced": "Расширенные", - "advanced_settings_beta_timeline_subtitle": "Попробуйте новый функционал приложения", - "advanced_settings_beta_timeline_title": "Бета-версия временной шкалы", - "advanced_settings_enable_alternate_media_filter_subtitle": "Используйте этот параметр для фильтрации медиафайлов во время синхронизации на основе альтернативных критериев. Пробуйте только в том случае, если у вас есть проблемы с обнаружением приложением всех альбомов.", - "advanced_settings_enable_alternate_media_filter_title": "[ЭКСПЕРИМЕНТАЛЬНО] Использование фильтра синхронизации альбомов альтернативных устройств", + "advanced_settings_enable_alternate_media_filter_subtitle": "Подбор объектов для синхронизации на основе альтернативных критериев. Пробуйте включать только в том случае, если в приложении есть проблемы с обнаружением всех альбомов.", + "advanced_settings_enable_alternate_media_filter_title": "[ЭКСПЕРИМЕНТАЛЬНО] Использование альтернативного способа синхронизации альбомов на устройстве", "advanced_settings_log_level_title": "Уровень логирования: {level}", "advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают локальные миниатюры. Активируйте эту настройку, чтобы изображения всегда загружались с сервера.", "advanced_settings_prefer_remote_title": "Предпочитать фото на сервере", "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом", "advanced_settings_proxy_headers_title": "Заголовки прокси", + "advanced_settings_readonly_mode_subtitle": "Включает режим, в котором можно только просматривать объекты. Функции выбора нескольких объектов, публикации, трансляции и удаления будут недоступны. Включить/отключить этот режим можно удерживая значок аватара пользователя на главном экране.", + "advanced_settings_readonly_mode_title": "Режим «только просмотр»", "advanced_settings_self_signed_ssl_subtitle": "Пропускать проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.", "advanced_settings_self_signed_ssl_title": "Разрешить самоподписанные SSL-сертификаты", - "advanced_settings_sync_remote_deletions_subtitle": "Автоматически удалять или восстанавливать объект на этом устройстве, когда это действие выполняется через веб-интерфейс", - "advanced_settings_sync_remote_deletions_title": "Синхронизация удаленных удалений [ЭКСПЕРИМЕНТАЛЬНО]", + "advanced_settings_sync_remote_deletions_subtitle": "Автоматически удалять или восстанавливать объекты на этом устройстве, когда это действие выполняется через веб-интерфейс", + "advanced_settings_sync_remote_deletions_title": "[ЭКСПЕРИМЕНТАЛЬНО] Синхронизация удаления объектов", "advanced_settings_tile_subtitle": "Расширенные настройки", - "advanced_settings_troubleshooting_subtitle": "Включить расширенные возможности для решения проблем", - "advanced_settings_troubleshooting_title": "Решение проблем", + "advanced_settings_troubleshooting_subtitle": "Включить расширенные возможности для диагностики и решения проблем", + "advanced_settings_troubleshooting_title": "Режим диагностики", "age_months": "{months, plural, one {# месяц} many {# месяцев} other {# месяца}}", "age_year_months": "1 год {months, plural, one {# месяц} many {# месяцев} other {# месяца}}", "age_years": "{years, plural, one {# год} many {# лет} other {# года}}", "album_added": "Альбом добавлен", - "album_added_notification_setting_description": "Получать уведомление по электронной почте, когда вы добавлены к общему альбому", + "album_added_notification_setting_description": "Получать уведомление по электронной почте, когда вам предоставили доступ в общий альбом", "album_cover_updated": "Обложка альбома обновлена", "album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?", "album_delete_confirmation_description": "Если альбом был общим, другие пользователи больше не смогут получить к нему доступ.", @@ -423,8 +431,9 @@ "album_remove_user_confirmation": "Вы уверены, что хотите удалить пользователя {user}?", "album_search_not_found": "Не найдено альбомов по вашему запросу", "album_share_no_users": "Нет доступных пользователей, с которыми можно поделиться альбомом.", + "album_summary": "Информация об альбоме", "album_updated": "Альбом обновлён", - "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых ресурсов в общий альбом", + "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых объектов в общий альбом", "album_user_left": "Вы покинули {album}", "album_user_removed": "Пользователь {user} удален", "album_viewer_appbar_delete_confirm": "Вы уверены, что хотите удалить альбом из своей учетной записи?", @@ -446,7 +455,7 @@ "all_albums": "Все альбомы", "all_people": "Все люди", "all_videos": "Все видео", - "allow_dark_mode": "Разрешить темный режим", + "allow_dark_mode": "Разрешить тёмный режим", "allow_edits": "Разрешить редактирование", "allow_public_user_to_download": "Разрешить скачивание", "allow_public_user_to_upload": "Разрешить добавление файлов", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Выйти", "app_settings": "Параметры приложения", "appears_in": "Добавлено в", + "apply_count": "Применить ({count, number})", "archive": "Архив", "archive_action_prompt": "Объекты добавлены в Архив ({count} шт.)", "archive_or_unarchive_photo": "Архивировать или разархивировать фото", @@ -493,10 +503,12 @@ "asset_restored_successfully": "Объект успешно восстановлен", "asset_skipped": "Пропущено", "asset_skipped_in_trash": "В корзине", + "asset_trashed": "Объект удалён", + "asset_troubleshoot": "Данные для диагностики", "asset_uploaded": "Загружено", "asset_uploading": "Загрузка…", - "asset_viewer_settings_subtitle": "Настройка параметров отображения", - "asset_viewer_settings_title": "Просмотр изображений", + "asset_viewer_settings_subtitle": "Параметры отображения", + "asset_viewer_settings_title": "Просмотр объектов", "assets": "Объекты", "assets_added_count": "{count, plural, one {Добавлен # объект} many {Добавлено # объектов} other {Добавлено # объекта}}", "assets_added_to_album_count": "В альбом {count, plural, one {добавлен # объект} many {добавлено # объектов} other {добавлено # объекта}}", @@ -526,24 +538,27 @@ "autoplay_slideshow": "Автовоспроизведение слайдшоу", "back": "Назад", "back_close_deselect": "Назад, закрыть или отменить выбор", + "background_backup_running_error": "Выполняется фоновое резервное копирование, запуск вручную пока невозможен", "background_location_permission": "Доступ к местоположению в фоне", "background_location_permission_content": "Чтобы считывать имя Wi-Fi сети в фоне, приложению *всегда* необходим доступ к точному местоположению устройства", + "background_options": "Выполнение фоновых задач", "backup": "Резервное копирование", "backup_album_selection_page_albums_device": "Альбомы на устройстве ({count})", "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить, дважды, чтобы исключить", - "backup_album_selection_page_assets_scatter": "Ваши изображения и видео могут находиться в разных альбомах. Вы можете выбрать, какие альбомы включить, а какие исключить из резервного копирования.", + "backup_album_selection_page_assets_scatter": "Ваши фото и видео могут находиться в разных альбомах/папках на устройстве. Вы можете выбрать, какие альбомы включить, а какие исключить из резервного копирования.", "backup_album_selection_page_select_albums": "Выбор альбомов", - "backup_album_selection_page_selection_info": "Информация о выборе", + "backup_album_selection_page_selection_info": "Выбранные альбомы", "backup_album_selection_page_total_assets": "Всего уникальных объектов", + "backup_albums_sync": "Синхронизация альбомов", "backup_all": "Все", "backup_background_service_backup_failed_message": "Не удалось выполнить резервное копирование. Повторная попытка…", "backup_background_service_connection_failed_message": "Не удалось подключиться к серверу. Повторная попытка…", "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_in_progress_notification": "Резервное копирование объектов…", "backup_background_service_upload_failure_notification": "Ошибка загрузки {filename}", - "backup_controller_page_albums": "Резервное копирование альбомов", + "backup_controller_page_albums": "Альбомы", "backup_controller_page_background_app_refresh_disabled_content": "Включите фоновое обновление приложения в Настройки > Общие > Фоновое обновление приложений, чтобы использовать фоновое резервное копирование.", "backup_controller_page_background_app_refresh_disabled_title": "Фоновое обновление отключено", "backup_controller_page_background_app_refresh_enable_button_text": "Перейти в настройки", @@ -553,15 +568,15 @@ "backup_controller_page_background_battery_info_title": "Оптимизация батареи", "backup_controller_page_background_charging": "Только во время зарядки", "backup_controller_page_background_configure_error": "Не удалось настроить фоновую службу", - "backup_controller_page_background_delay": "Отложить резервное копирование новых объектов: {duration}", + "backup_controller_page_background_delay": "Задержка перед загрузкой новых объектов: {duration}", "backup_controller_page_background_description": "Включите фоновую службу для автоматического резервного копирования любых новых объектов без необходимости открывать приложение", "backup_controller_page_background_is_off": "Автоматическое резервное копирование в фоновом режиме отключено", "backup_controller_page_background_is_on": "Автоматическое резервное копирование в фоновом режиме включено", "backup_controller_page_background_turn_off": "Выключить фоновую службу", "backup_controller_page_background_turn_on": "Включить фоновую службу", "backup_controller_page_background_wifi": "Только через Wi-Fi", - "backup_controller_page_backup": "Резервное копирование", - "backup_controller_page_backup_selected": "Выбрано: ", + "backup_controller_page_backup": "Загружено", + "backup_controller_page_backup_selected": "Выбраны: ", "backup_controller_page_backup_sub": "Загруженные фото и видео", "backup_controller_page_created": "Создано: {date}", "backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты при открытии приложения.", @@ -570,7 +585,7 @@ "backup_controller_page_filename": "Имя файла: {filename} [{size}]", "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Информация о резервном копировании", - "backup_controller_page_none_selected": "Ничего не выбрано", + "backup_controller_page_none_selected": "Не выбрано", "backup_controller_page_remainder": "Осталось", "backup_controller_page_remainder_sub": "Фото и видео для загрузки", "backup_controller_page_server_storage": "Хранилище на сервере", @@ -584,6 +599,7 @@ "backup_controller_page_turn_on": "Включить", "backup_controller_page_uploading_file_info": "Информация о загружаемом файле", "backup_err_only_album": "Невозможно удалить единственный альбом", + "backup_error_sync_failed": "Сбой синхронизации. Невозможно запустить резервное копирование.", "backup_info_card_assets": "объектов", "backup_manual_cancelled": "Отменено", "backup_manual_in_progress": "Загрузка в процессе. Попробуйте позже", @@ -594,8 +610,6 @@ "backup_setting_subtitle": "Настройка активного и фонового резервного копирования", "backup_settings_subtitle": "Настройка загрузки объектов", "backward": "Назад", - "beta_sync": "Статус бета-синхронизации", - "beta_sync_subtitle": "Управление новой системой синхронизации", "biometric_auth_enabled": "Биометрическая аутентификация включена", "biometric_locked_out": "Вам закрыт доступ к биометрической аутентификации", "biometric_no_options": "Биометрическая аутентификация недоступна", @@ -634,8 +648,8 @@ "cannot_merge_people": "Невозможно объединить людей", "cannot_undo_this_action": "Это действие нельзя отменить!", "cannot_update_the_description": "Невозможно обновить описание", - "cast": "Транслировать", - "cast_description": "Настройка доступных целей трансляции", + "cast": "Трансляция", + "cast_description": "Выбор доступных способов для трансляции", "change_date": "Изменить дату", "change_description": "Изменить описание", "change_display_order": "Изменить порядок отображения", @@ -653,6 +667,8 @@ "change_pin_code": "Изменить PIN-код", "change_your_password": "Изменить свой пароль", "changed_visibility_successfully": "Видимость успешно изменена", + "charging": "При зарядке", + "charging_requirement_mobile_backup": "Запускать резервное копирование только во время зарядки", "check_corrupt_asset_backup": "Проверка поврежденных резервных копий", "check_corrupt_asset_backup_button": "Проверить", "check_corrupt_asset_backup_description": "Запускайте проверку только через Wi-Fi и после создания резервной копии всех объектов. Операция может занять несколько минут.", @@ -680,8 +696,8 @@ "color": "Цвет", "color_theme": "Цветовая тема", "comment_deleted": "Комментарий удалён", - "comment_options": "Параметры комментариев", - "comments_and_likes": "Комментарии и лайки", + "comment_options": "Действия с комментарием", + "comments_and_likes": "Комментарии и отметки \"нравится\"", "comments_are_disabled": "Комментарии отключены", "common_create_new_album": "Создать новый альбом", "common_server_error": "Пожалуйста, проверьте подключение к сети и убедитесь, что ваш сервер доступен, а версии приложения и сервера — совместимы.", @@ -689,7 +705,7 @@ "confirm": "Подтвердить", "confirm_admin_password": "Подтвердите пароль администратора", "confirm_delete_face": "Удалить лицо человека {name} из этого объекта?", - "confirm_delete_shared_link": "Вы уверены, что хотите удалить эту публичную ссылку?", + "confirm_delete_shared_link": "Вы действительно хотите удалить эту публичную ссылку?", "confirm_keep_this_delete_others": "Все остальные объекты в группе будут удалены, кроме этого объекта. Вы уверены, что хотите продолжить?", "confirm_new_pin_code": "Подтвердите новый PIN-код", "confirm_password": "Подтвердите пароль", @@ -705,12 +721,12 @@ "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": "Поделиться ссылкой", + "control_bottom_app_bar_share_link": "Создать ссылку", "control_bottom_app_bar_share_to": "Поделиться с", "control_bottom_app_bar_trash_from_immich": "В корзину", "copied_image_to_clipboard": "Изображение скопировано в буфер обмена.", "copied_to_clipboard": "Скопировано в буфер обмена!", - "copy_error": "Ошибка копирования", + "copy_error": "Скопировать ошибку", "copy_file_path": "Копировать путь к файлу", "copy_image": "Копировать", "copy_link": "Копировать ссылку", @@ -726,7 +742,7 @@ "create_library": "Создать библиотеку", "create_link": "Создать ссылку", "create_link_to_share": "Создать ссылку общего доступа", - "create_link_to_share_description": "Разрешить всем, у кого есть ссылка, просмотреть выбранные фотографии", + "create_link_to_share_description": "Разрешить всем, у кого есть ссылка, просматривать выбранные фотографии", "create_new": "СОЗДАТЬ НОВЫЙ", "create_new_person": "Добавить нового человека", "create_new_person_hint": "Назначить выбранные объекты на нового человека", @@ -739,6 +755,7 @@ "create_user": "Создать пользователя", "created": "Создан", "created_at": "Создан", + "creating_linked_albums": "Создание связанных альбомов...", "crop": "Обрезать", "curated_object_page_title": "Предметы", "current_device": "Текущее устройство", @@ -750,7 +767,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Тёмная", - "dark_theme": "Тёмная тема", + "dark_theme": "Включить/выключить тёмную тему", "date_after": "Дата после", "date_and_time": "Дата и Время", "date_before": "Дата до", @@ -767,7 +784,7 @@ "default_locale": "Дата и время по умолчанию", "default_locale_description": "Использовать формат даты и времени в соответствии с языковым стандартом вашего браузера", "delete": "Удалить", - "delete_action_confirmation_message": "Вы действительно хотите удалить этот объект? Это действие переместит объект в корзину сервера и предложит удалить его локально.", + "delete_action_confirmation_message": "Вы действительно хотите удалить этот объект? Это действие переместит объект в корзину сервера и попробует удалить его локально.", "delete_action_prompt": "Объекты удалены ({count} шт.)", "delete_album": "Удалить альбом", "delete_api_key_prompt": "Вы действительно хотите удалить этот API ключ?", @@ -782,7 +799,7 @@ "delete_key": "Удалить ключ", "delete_library": "Удалить библиотеку", "delete_link": "Удалить ссылку", - "delete_local_action_prompt": "Объекты удалены локально ({count} шт.)", + "delete_local_action_prompt": "Объекты удалены с устройства ({count} шт.)", "delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии", "delete_local_dialog_ok_force": "Все равно удалить", "delete_others": "Удалить остальные", @@ -808,7 +825,7 @@ "discovered_devices": "Обнаруженные устройства", "dismiss_all_errors": "Сбросить все ошибки", "dismiss_error": "Сбросить ошибку", - "display_options": "Настройки отображения", + "display_options": "Дополнительно", "display_order": "Порядок отображения", "display_original_photos": "Отображение оригинальных фотографий", "display_original_photos_setting_description": "Открывать при просмотре оригинал фотографии вместо миниатюры, если исходный формат поддерживается браузером. Возможно снижение скорости отображения фотографий.", @@ -824,7 +841,7 @@ "download_failed": "Загрузка не удалась", "download_finished": "Загрузка окончена", "download_include_embedded_motion_videos": "Встроенные видео", - "download_include_embedded_motion_videos_description": "Включить видео, встроенные в живые фото, в виде отдельного файла", + "download_include_embedded_motion_videos_description": "Сохранять видео, встроенные в живые фото, в виде отдельных файлов", "download_notfound": "Загрузка не найдена", "download_paused": "Загрузка приостановлена", "download_settings": "Скачивание", @@ -840,11 +857,11 @@ "duplicates": "Дубликаты", "duplicates_description": "Просмотрите найденные дубликаты и в каждой группе укажите, какие объекты оставить, а какие удалить", "duration": "Продолжительность", - "edit": "Редактировать", - "edit_album": "Редактировать альбом", + "edit": "Изменить", + "edit_album": "Изменить альбом", "edit_avatar": "Изменить аватар", "edit_birthday": "Изменить дату рождения", - "edit_date": "редактировать дату", + "edit_date": "Изменить дату", "edit_date_and_time": "Изменить дату и время", "edit_date_and_time_action_prompt": "Дата и время изменены у {count} объектов", "edit_date_and_time_by_offset": "Изменить дату по смещению", @@ -856,14 +873,14 @@ "edit_import_path": "Изменить путь импорта", "edit_import_paths": "Изменить путь импорта", "edit_key": "Изменить ключ", - "edit_link": "Редактировать ссылку", - "edit_location": "Редактировать местоположение", + "edit_link": "Изменить ссылку", + "edit_location": "Изменить местоположение", "edit_location_action_prompt": "Места изменены ({count} шт.)", "edit_location_dialog_title": "Местоположение", - "edit_name": "Редактировать имя", - "edit_people": "Редактировать людей", + "edit_name": "Изменить имя", + "edit_people": "Изменить людей", "edit_tag": "Изменить тег", - "edit_title": "Редактировать Заголовок", + "edit_title": "Изменить заголовок", "edit_user": "Изменить пользователя", "edited": "Отредактировано", "editor": "Редактор", @@ -877,7 +894,7 @@ "empty_trash": "Очистить корзину", "empty_trash_confirmation": "Вы действительно хотите очистить корзину? Все объекты в ней будут навсегда удалены из Immich.\nВы не сможете отменить это действие!", "enable": "Включить", - "enable_backup": "Включить резервное копирование", + "enable_backup": "Активировать", "enable_biometric_auth_description": "Введите свой PIN-код для включения биометрической аутентификации", "enabled": "Включено", "end_date": "Дата окончания", @@ -888,28 +905,30 @@ "error": "Ошибка", "error_change_sort_album": "Не удалось изменить порядок сортировки альбома", "error_delete_face": "Ошибка при удалении лица из объекта", + "error_getting_places": "Ошибка получения мест", "error_loading_image": "Ошибка при загрузке изображения", + "error_loading_partners": "Ошибка загрузки партнёров: {error}", "error_saving_image": "Ошибка: {error}", "error_tag_face_bounding_box": "Ошибка при добавлении отметки - не удалось получить координаты рамки лица", "error_title": "Ошибка - Что-то пошло не так", "errors": { "cannot_navigate_next_asset": "Не удалось перейти к следующему объекту", - "cannot_navigate_previous_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": "Не удается получить количество комментариев", "cant_search_people": "Не удается выполнить поиск людей", "cant_search_places": "Не удается выполнить поиск мест", - "error_adding_assets_to_album": "Ошибка при добавлении ресурсов в альбом", + "error_adding_assets_to_album": "Ошибка при добавлении объектов в альбом", "error_adding_users_to_album": "Ошибка при добавлении пользователей в альбом", "error_deleting_shared_user": "Ошибка при удалении пользователя с общим доступом", "error_downloading": "Ошибка при загрузке {filename}", "error_hiding_buy_button": "Ошибка скрытия кнопки", - "error_removing_assets_from_album": "Ошибка при удалении ресурсов из альбома, проверьте консоль для получения дополнительной информации", - "error_selecting_all_assets": "Ошибка при выборе всех ресурсов", + "error_removing_assets_from_album": "Ошибка при удалении объектов из альбома, проверьте консоль для получения дополнительной информации", + "error_selecting_all_assets": "Ошибка при выборе всех объектов", "exclusion_pattern_already_exists": "Такая модель исключения уже существует.", "failed_to_create_album": "Не удалось создать альбом", "failed_to_create_shared_link": "Не удалось создать публичную ссылку", @@ -1001,7 +1020,7 @@ "unable_to_scan_library": "Не удалось просканировать библиотеку", "unable_to_set_feature_photo": "Не удалось установить фотографию на обложку", "unable_to_set_profile_picture": "Не удалось установить фото профиля", - "unable_to_submit_job": "Не удалось отправить задание", + "unable_to_submit_job": "Не удалось отправить задачу на выполнение", "unable_to_trash_asset": "Не удалось переместить объект в корзину", "unable_to_unlink_account": "Не удалось отсоединить учётную запись", "unable_to_unlink_motion_video": "Не удалось отсоединить движущееся видео", @@ -1040,7 +1059,7 @@ "external": "Внешний", "external_libraries": "Внешние библиотеки", "external_network": "Внешняя сеть", - "external_network_sheet_info": "Когда устройство не подключено к выбранной Wi-Fi сети, приложение будет пытаться подключиться к серверу по адресам ниже, сверху вниз, до успешного подключения", + "external_network_sheet_info": "Когда устройство не подключено к указанной Wi-Fi сети, приложение будет пытаться подключиться к серверу по адресам ниже, сверху вниз до успешного подключения", "face_unassigned": "Не назначено", "failed": "Ошибка", "failed_to_authenticate": "Ошибка аутентификации", @@ -1053,6 +1072,7 @@ "favorites_page_no_favorites": "В избранном сейчас пусто", "feature_photo_updated": "Избранное фото обновлено", "features": "Дополнительные возможности", + "features_in_development": "Функции в разработке", "features_setting_description": "Управление дополнительными возможностями приложения", "file_name": "Имя файла", "file_name_or_extension": "Имя файла или расширение", @@ -1067,18 +1087,21 @@ "folder": "Папка", "folder_not_found": "Папка не найдена", "folders": "Папки", - "folders_feature_description": "Просмотр папок с фотографиями и видео в файловой системе", + "folders_feature_description": "Просмотр папок с фото и видео в файловой системе", "forgot_pin_code_question": "Забыли PIN-код?", "forward": "Вперёд", "gcast_enabled": "Google Cast", - "gcast_enabled_description": "Этот функционал требует загрузки внешних ресурсов с серверов Google.", + "gcast_enabled_description": "Для работы требуется загрузка внешних ресурсов с серверов Google.", "general": "Общие", + "geolocation_instruction_location": "Выберите объект с имеющимися координатами, чтобы использовать их, либо вручную укажите место на карте", "get_help": "Получить помощь", "get_wifiname_error": "Не удалось получить имя Wi-Fi сети. Убедитесь, что вы подключены к сети и предоставили приложению необходимые разрешения", "getting_started": "Старт", "go_back": "Назад", "go_to_folder": "Перейти в папку", "go_to_search": "Перейти к поиску", + "gps": "Есть координаты", + "gps_missing": "Нет координат", "grant_permission": "Предоставить разрешение", "group_albums_by": "Группировать альбомы по...", "group_country": "Группировать по странам", @@ -1089,8 +1112,8 @@ "haptic_feedback_switch": "Включить тактильную отдачу", "haptic_feedback_title": "Тактильная отдача", "has_quota": "Квота", - "hash_asset": "Хешированный объект", - "hashed_assets": "Хешированные объекты", + "hash_asset": "Хеширование объектов", + "hashed_assets": "Хеши", "hashing": "Хеширование", "header_settings_add_header_tip": "Добавить заголовок", "header_settings_field_validator_msg": "Значение не может быть пустым", @@ -1108,14 +1131,14 @@ "home_page_add_to_album_conflicts": "Добавлено {added} медиа в альбом {album}. {failed} медиа уже в альбоме.", "home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропуск", "home_page_add_to_album_success": "Добавлено {added} медиа в альбом {album}.", - "home_page_album_err_partner": "Пока нельзя добавить медиа партнера в альбом, пропуск", + "home_page_album_err_partner": "Невозможно добавить объекты партнёра в альбом, пропуск", "home_page_archive_err_local": "Пока нельзя добавить локальные файлы в архив, пропуск", - "home_page_archive_err_partner": "Невозможно архивировать медиа партнера, пропуск", + "home_page_archive_err_partner": "Невозможно добавить объекты партнёра в архив, пропуск", "home_page_building_timeline": "Построение хронологии", - "home_page_delete_err_partner": "Невозможно удалить медиа партнера, пропуск", + "home_page_delete_err_partner": "Невозможно удалить объекты партнёра, пропуск", "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": "Невозможно переместить объекты партнёра в личную папку, пропуск", @@ -1150,8 +1173,8 @@ "in_albums": "В {count, plural, one {# альбоме} other {# альбомах}}", "in_archive": "В архиве", "include_archived": "Отображать архив", - "include_shared_albums": "Включать общие альбомы", - "include_shared_partner_assets": "Включать общие ресурсы партнера", + "include_shared_albums": "Включать объекты общих альбомов", + "include_shared_partner_assets": "Включать объекты партнёров", "individual_share": "Индивидуальная подборка", "individual_shares": "Подборки", "info": "Информация", @@ -1163,7 +1186,7 @@ }, "invalid_date": "Неверная дата", "invalid_date_format": "Неверный формат даты", - "invite_people": "Пригласить", + "invite_people": "Пригласить участника", "invite_to_album": "Пригласить в альбом", "ios_debug_info_fetch_ran_at": "Выборка запущена {dateTime}", "ios_debug_info_last_sync_at": "Последняя синхронизация {dateTime}", @@ -1181,7 +1204,7 @@ "language": "Язык", "language_no_results_subtitle": "Попробуйте скорректировать поисковый запрос", "language_no_results_title": "Языков не найдено", - "language_search_hint": "Поиск языков...", + "language_search_hint": "Поиск языка...", "language_setting_description": "Выберите предпочитаемый вами язык", "large_files": "Файлы наибольшего размера", "last": "Последний", @@ -1191,10 +1214,10 @@ "leave": "Покинуть", "leave_album": "Покинуть альбом", "lens_model": "Модель объектива", - "let_others_respond": "Позволять другим откликаться", + "let_others_respond": "Разрешить другим пользователям добавлять комментарии и отметки \"нравится\"", "level": "Уровень", "library": "Библиотека", - "library_options": "Опции библиотеки", + "library_options": "Действия с библиотекой", "library_page_device_albums": "Альбомы на устройстве", "library_page_new_album": "Новый альбом", "library_page_sort_asset_count": "Количество объектов", @@ -1212,10 +1235,11 @@ "loading": "Загрузка", "loading_search_results_failed": "Загрузка результатов поиска не удалась", "local": "На устройстве", - "local_asset_cast_failed": "Невозможно транслировать объект, который ещё не загружен на сервер", + "local_asset_cast_failed": "Невозможна трансляция объектов, которые ещё не загружены на сервер", "local_assets": "Объекты на устройстве", + "local_media_summary": "Информация об объекте на устройстве", "local_network": "Локальная сеть", - "local_network_sheet_info": "Приложение будет подключаться к серверу по этому адресу, когда устройство подключено к выбранной Wi-Fi сети", + "local_network_sheet_info": "Приложение будет подключаться к серверу по этому адресу, когда устройство подключено к указанной Wi-Fi сети", "location_permission": "Доступ к местоположению", "location_permission_content": "Чтобы использовать функцию автоматического переключения, Immich необходимо разрешение на точное определение местоположения, чтобы оно могло считывать название текущей Wi-Fi сети", "location_picker_choose_on_map": "Выбрать на карте", @@ -1225,6 +1249,7 @@ "location_picker_longitude_hint": "Введите долготу", "lock": "Заблокировать", "locked_folder": "Личная папка", + "log_detail_title": "Детали события", "log_out": "Выйти", "log_out_all_devices": "Завершить сеансы на всех устройствах", "logged_in_as": "Авторизован как {user}", @@ -1255,6 +1280,7 @@ "login_password_changed_success": "Пароль успешно обновлен", "logout_all_device_confirmation": "Вы действительно хотите завершить все сеансы, кроме текущего?", "logout_this_device_confirmation": "Вы действительно хотите завершить сеанс на этом устройстве?", + "logs": "Журнал событий", "longitude": "Долгота", "look": "Просмотр", "loop_videos": "Циклическое воспроизведение видео", @@ -1262,8 +1288,9 @@ "main_branch_warning": "Вы используете версию приложения для разработки. Настоятельно рекомендуется перейти на релизную версию приложения!", "main_menu": "Главное меню", "make": "Производитель", + "manage_geolocation": "Управление местами съёмки", "manage_shared_links": "Управление публичными ссылками", - "manage_sharing_with_partners": "Управление обменом информацией с партнерами. Эта функция позволяет вашему партнеру видеть ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине", + "manage_sharing_with_partners": "Функция совместного доступа к фото и видео, позволяющая видеть объекты партнёров, а также предоставлять доступ к своим", "manage_the_app_settings": "Управление настройками приложения", "manage_your_account": "Управление учётной записью", "manage_your_api_keys": "Управление API ключами для взаимодействия с другими программами", @@ -1296,6 +1323,7 @@ "mark_as_read": "Отметить как прочитанное", "marked_all_as_read": "Отмечены как прочитанные", "matches": "Совпадения", + "matching_assets": "Соответствующие объекты", "media_type": "Тип медиа", "memories": "Воспоминания", "memories_all_caught_up": "Это всё на сегодня", @@ -1307,7 +1335,7 @@ "memory_lane_title": "Воспоминание {title}", "menu": "Меню", "merge": "Объединить", - "merge_people": "Объединить людей", + "merge_people": "Объединить с другим", "merge_people_limit": "Вы можете объединять до 5 лиц за один раз", "merge_people_prompt": "Вы хотите объединить этих людей? Это действие необратимо.", "merge_people_successfully": "Лица людей успешно объединены", @@ -1319,11 +1347,11 @@ "model": "Модель", "month": "Месяц", "monthly_title_text_date_format": "MMMM y", - "more": "Больше", + "more": "Дополнительные действия", "move": "Переместить", - "move_off_locked_folder": "Переместить из личной папки", + "move_off_locked_folder": "Убрать из личной папки", "move_to_lock_folder_action_prompt": "Объекты добавлены в личную папку ({count} шт.)", - "move_to_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 {# объекта перемещены}} в библиотеку", @@ -1336,6 +1364,7 @@ "name_or_nickname": "Имя или ник", "network_requirement_photos_upload": "Использовать мобильный интернет для загрузки фото", "network_requirement_videos_upload": "Использовать мобильный интернет для загрузки видео", + "network_requirements": "Требования к сети", "network_requirements_updated": "Требования к сети изменились, сброс очереди загрузки", "networking_settings": "Сеть", "networking_subtitle": "Настройка подключения к серверу", @@ -1346,6 +1375,7 @@ "new_person": "Новый человек", "new_pin_code": "Новый PIN-код", "new_pin_code_subtitle": "Это ваш первый доступ к личной папке. Создайте PIN-код для защищенного доступа к этой странице.", + "new_timeline": "Новая лента", "new_user_created": "Новый пользователь создан", "new_version_available": "ДОСТУПНА НОВАЯ ВЕРСИЯ", "newest_first": "Сначала новые", @@ -1359,23 +1389,28 @@ "no_assets_message": "НАЖМИТЕ ДЛЯ ЗАГРУЗКИ ВАШЕГО ПЕРВОГО ФОТО", "no_assets_to_show": "Медиа отсутствуют", "no_cast_devices_found": "Не найдено устройств для трансляции", + "no_checksum_local": "Контрольные суммы отсутствуют - невозможно получить объекты на устройстве", + "no_checksum_remote": "Контрольные суммы отсутствуют - невозможно получить объекты с сервера", "no_duplicates_found": "Дубликатов не обнаружено.", "no_exif_info_available": "Нет доступной информации exif", "no_explore_results_message": "Загружайте больше фотографий, чтобы наслаждаться вашей коллекцией.", - "no_favorites_message": "Добавляйте в избранное, чтобы быстро найти свои лучшие фотографии и видео", + "no_favorites_message": "Добавляйте объекты в избранное, чтобы быстрее находить свои лучшие фото и видео", "no_libraries_message": "Создайте внешнюю библиотеку для просмотра в Immich сторонних фотографий и видео", + "no_local_assets_found": "На устройстве не найдено объектов с такой контрольной суммой", "no_locked_photos_message": "Фото и видео, перемещенные в личную папку, скрыты и не отображаются при просмотре библиотеки.", "no_name": "Нет имени", "no_notifications": "Нет уведомлений", "no_people_found": "Никого не найдено", "no_places": "Нет мест", + "no_remote_assets_found": "На сервере не найдено объектов с такой контрольной суммой", "no_results": "Нет результатов", "no_results_description": "Попробуйте использовать синоним или более общее ключевое слово", "no_shared_albums_message": "Создайте альбом для обмена фотографиями и видеозаписями с людьми в вашей сети", "no_uploads_in_progress": "Нет активных загрузок", + "not_available": "Нет данных", "not_in_any_album": "Ни в одном альбоме", "not_selected": "Не выбрано", - "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить метку хранилища к ранее загруженным ресурсам, запустите", + "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить метку хранилища к ранее загруженным объектам, запустите", "notes": "Примечание", "nothing_here_yet": "Здесь пока ничего нет", "notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».", @@ -1405,8 +1440,10 @@ "open_in_map_view": "Открыть в режиме просмотра карты", "open_in_openstreetmap": "Открыть в OpenStreetMap", "open_the_search_filters": "Открыть фильтры поиска", - "options": "Опции", + "options": "Параметры", "or": "или", + "organize_into_albums": "Распределить по альбомам", + "organize_into_albums_description": "Добавить уже существующие объекты в альбомы, используя текущие настройки синхронизации", "organize_your_library": "Приведите в порядок свою библиотеку", "original": "оригинал", "other": "Другое", @@ -1415,18 +1452,18 @@ "other_variables": "Другие переменные", "owned": "Мои", "owner": "Владелец", - "partner": "Партнер", - "partner_can_access": "{partner} имеет доступ", - "partner_can_access_assets": "Все ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине", - "partner_can_access_location": "Местоположение, где были сделаны ваши фотографии", - "partner_list_user_photos": "Фотографии пользователя {user}", + "partner": "Партнёр", + "partner_can_access": "Пользователю {partner} доступны", + "partner_can_access_assets": "Все ваши фото и видео, кроме тех, что находятся в архиве и корзине", + "partner_can_access_location": "Места, где были сделаны ваши фото и видео", + "partner_list_user_photos": "Фото и видео пользователя {user}", "partner_list_view_all": "Посмотреть все", - "partner_page_empty_message": "У вашего партнёра еще нет доступа к вашим фото.", + "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": "Пользователь {partner} больше не сможет получить доступ к вашим фото.", + "partner_page_stop_sharing_content": "Пользователь {partner} больше не будет иметь доступ к вашим фото и видео.", "partner_sharing": "Совместное использование", "partners": "Партнёры", "password": "Пароль", @@ -1446,7 +1483,7 @@ "pending": "Ожидает", "people": "Люди", "people_edits_count": "{count, plural, one {Изменён # человек} many {Изменено # человек} other {Изменено # человека}}", - "people_feature_description": "Просмотр фотографий и видео, сгруппированных по людям", + "people_feature_description": "Просмотр фото и видео, сгруппированных по людям", "people_sidebar_description": "Отображать пункт меню \"Люди\" в боковой панели", "permanent_deletion_warning": "Предупреждение об удалении", "permanent_deletion_warning_setting_description": "Предупреждать перед безвозвратным удалением объектов", @@ -1492,6 +1529,7 @@ "port": "Порт", "preferences_settings_subtitle": "Настройка внешнего вида", "preferences_settings_title": "Параметры", + "preparing": "Подготовка", "preset": "Предустановленные варианты", "preview": "Предварительный просмотр", "previous": "Предыдущее", @@ -1503,11 +1541,12 @@ "primary": "Главное", "privacy": "Конфиденциальность", "profile": "Профиль", - "profile_drawer_app_logs": "Журнал", + "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_readonly_mode": "Включён режим «только просмотр». Удерживайте значок аватара пользователя для отключения.", "profile_drawer_server_out_of_date_major": "Версия сервера устарела. Пожалуйста, обновите его.", "profile_drawer_server_out_of_date_minor": "Версия сервера устарела. Пожалуйста, обновите его.", "profile_image_of_user": "Изображение профиля {user}", @@ -1546,17 +1585,21 @@ "purchase_server_description_2": "Состояние поддержки", "purchase_server_title": "Сервер", "purchase_settings_server_activated": "Ключом продукта управляет администратор сервера", + "query_asset_id": "Идентификатор исходного объекта", "queue_status": "В очереди {count}/{total}", - "rating": "Рейтинг звёзд", + "rating": "Рейтинг", "rating_clear": "Очистить рейтинг", "rating_count": "{count, plural, one {# звезда} many {# звезд} other {# звезды}}", - "rating_description": "Показывать рейтинг в панели информации", - "reaction_options": "Опции реакций", - "read_changelog": "Прочитать список изменений", + "rating_description": "Система оценки объектов в панели информации", + "reaction_options": "Действия с отметкой", + "read_changelog": "История релизов", + "readonly_mode_disabled": "Режим «только просмотр» отключён", + "readonly_mode_enabled": "Режим «только просмотр» включён", + "ready_for_upload": "Готово к загрузке", "reassign": "Переназначить", "reassigned_assets_to_existing_person": "Лица на {count, plural, one {# объекте} other {# объектах}} переназначены на {name, select, null {другого человека} other {человека с именем {name}}}", "reassigned_assets_to_new_person": "Лица на {count, plural, one {# объекте} other {# объектах}} переназначены на нового человека", - "reassing_hint": "Назначить выбранные ресурсы указанному человеку", + "reassing_hint": "Назначить выбранные объекты указанному человеку", "recent": "Недавние", "recent-albums": "Недавние альбомы", "recent_searches": "Недавние поисковые запросы", @@ -1577,6 +1620,7 @@ "regenerating_thumbnails": "Восстановление миниатюр", "remote": "На сервере", "remote_assets": "Объекты на сервере", + "remote_media_summary": "Информация об объекте на сервере", "remove": "Удалить", "remove_assets_album_confirmation": "Вы действительно хотите удалить {count, plural, one {# объект} many {# объектов} other {# объекта}} из альбома?", "remove_assets_shared_link_confirmation": "Вы действительно хотите удалить {count, plural, one {# объект} many {# объектов} other {# объекта}} из публичного доступа по этой ссылке?", @@ -1588,7 +1632,7 @@ "remove_from_favorites": "Удалить из избранного", "remove_from_lock_folder_action_prompt": "Объекты удалены из личной папки ({count} шт.)", "remove_from_locked_folder": "Удалить из личной папки", - "remove_from_locked_folder_confirmation": "Вы действительно хотите переместить эти фото и видео из личной папки? Они станут доступны в вашей библиотеке.", + "remove_from_locked_folder_confirmation": "Вы хотите убрать выделенные объекты из личной папки? Они снова станут доступны в вашей библиотеке.", "remove_from_shared_link": "Удалить из публичной ссылки", "remove_memory": "Удалить воспоминание", "remove_photo_from_memory": "Удалить фото из воспоминания", @@ -1618,7 +1662,7 @@ "reset_pin_code_success": "PIN-код успешно сброшен", "reset_pin_code_with_password": "Вы всегда можете сбросить PIN-код с помощью пароля", "reset_sqlite": "Очистить базу данных SQLite", - "reset_sqlite_confirmation": "Вы уверены, что хотите очистить базу данных SQLite? Вам потребуется выйти из системы и снова войти для повторной синхронизации данных.", + "reset_sqlite_confirmation": "Вы действительно хотите очистить базу данных SQLite? Вам потребуется выйти из приложения и снова войти для повторной синхронизации данных.", "reset_sqlite_success": "База данных SQLite успешно очищена", "reset_to_default": "Восстановление значений по умолчанию", "resolve_duplicates": "Устранить дубликаты", @@ -1629,6 +1673,7 @@ "restore_user": "Восстановить пользователя", "restored_asset": "Восстановленный объект", "resume": "Продолжить", + "resume_paused_jobs": "Возобновить выполнение {count, plural, one {# задачи} other {# задач}}", "retry_upload": "Повторить загрузку", "review_duplicates": "Разбор дубликатов", "review_large_files": "Обзор больших файлов", @@ -1641,7 +1686,7 @@ "saved_api_key": "API ключ изменён", "saved_profile": "Профиль сохранён", "saved_settings": "Настройки сохранены", - "say_something": "Скажите что-нибудь", + "say_something": "Напишите что-нибудь", "scaffold_body_error_occurred": "Возникла ошибка", "scan_all_libraries": "Сканировать все библиотеки", "scan_library": "Сканировать", @@ -1662,7 +1707,7 @@ "search_filter_camera_title": "Выберите тип камеры", "search_filter_date": "Дата", "search_filter_date_interval": "{start} — {end}", - "search_filter_date_title": "Выберите промежуток", + "search_filter_date_title": "Выберите период", "search_filter_display_option_not_in_album": "Не в альбоме", "search_filter_display_options": "Настройки отображения", "search_filter_filename": "Поиск по имени файла", @@ -1715,19 +1760,20 @@ "select_from_computer": "Выбрать с компьютера", "select_keep_all": "Выбрать все для сохранения", "select_library_owner": "Выберите владельца библиотеки", - "select_new_face": "Выбрать другое лицо", + "select_new_face": "Выбрать другого человека", "select_person_to_tag": "Выделите лицо человека, которого хотите отметить", "select_photos": "Выберите фотографии", "select_trash_all": "Выбрать все для удаления", "select_user_for_sharing_page_err_album": "Не удалось создать альбом", "selected": "Выбрано", "selected_count": "{count, plural, one {Выбран # объект} many {Выбрано # объектов} other {Выбрано # объекта}}", + "selected_gps_coordinates": "Выбранные координаты", "send_message": "Отправить сообщение", "send_welcome_email": "Отправить приветственное письмо", "server_endpoint": "Адрес сервера", "server_info_box_app_version": "Версия приложения", "server_info_box_server_url": "URL сервера", - "server_offline": "Сервер не в сети", + "server_offline": "Оффлайн", "server_online": "Сервер в сети", "server_privacy": "Конфиденциальность сервера", "server_stats": "Статистика сервера", @@ -1747,7 +1793,7 @@ "setting_image_viewer_preview_title": "Загружать уменьшенное изображение", "setting_image_viewer_title": "Изображения", "setting_languages_apply": "Применить", - "setting_languages_subtitle": "Изменить язык приложения", + "setting_languages_subtitle": "Изменение языка приложения", "setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {duration}", "setting_notifications_notify_hours": "{count} ч.", "setting_notifications_notify_immediately": "немедленно", @@ -1756,7 +1802,7 @@ "setting_notifications_notify_seconds": "{count} сек.", "setting_notifications_single_progress_subtitle": "Подробная информация о ходе загрузки для каждого объекта", "setting_notifications_single_progress_title": "Показать ход выполнения фонового резервного копирования", - "setting_notifications_subtitle": "Настройка параметров уведомлений", + "setting_notifications_subtitle": "Параметры уведомлений", "setting_notifications_total_progress_subtitle": "Общий прогресс загрузки (выполнено/всего объектов)", "setting_notifications_total_progress_title": "Показать общий прогресс фонового резервного копирования", "setting_video_viewer_looping_title": "Циклическое воспроизведение", @@ -1783,23 +1829,23 @@ "shared_by": "Поделился", "shared_by_user": "Владелец: {user}", "shared_by_you": "Вы поделились", - "shared_from_partner": "Фото от {partner}", + "shared_from_partner": "Пользователь {partner} предоставил вам доступ", "shared_intent_upload_button_progress_text": "{current} / {total} Загружено", "shared_link_app_bar_title": "Публичные ссылки", "shared_link_clipboard_copied_massage": "Скопировано в буфер обмена", "shared_link_clipboard_text": "Ссылка: {link}\nПароль: {password}", "shared_link_create_error": "Ошибка при создании публичной ссылки", - "shared_link_custom_url_description": "Доступ к этой общей ссылке с помощью заданного пользователем URL-адреса", - "shared_link_edit_description_hint": "Введите описание публичного доступа", + "shared_link_custom_url_description": "Пользовательский URL-адрес общего доступа", + "shared_link_edit_description_hint": "Введите описание", "shared_link_edit_expire_after_option_day": "1 день", "shared_link_edit_expire_after_option_days": "{count} дней", "shared_link_edit_expire_after_option_hour": "1 час", "shared_link_edit_expire_after_option_hours": "{count} часов", "shared_link_edit_expire_after_option_minute": "1 минуту", "shared_link_edit_expire_after_option_minutes": "{count} минут", - "shared_link_edit_expire_after_option_months": "{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_password_hint": "Защитите доступ паролем", "shared_link_edit_submit_button": "Обновить ссылку", "shared_link_error_server_url_fetch": "Невозможно запросить URL с сервера", "shared_link_expires_day": "Истечёт через {count} день", @@ -1814,13 +1860,13 @@ "shared_link_individual_shared": "Индивидуальный общий доступ", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Управление публичными ссылками", - "shared_link_options": "Параметры публичных ссылок", - "shared_link_password_description": "Требовать пароль для доступа к этой общей ссылке", + "shared_link_options": "Действия со ссылкой", + "shared_link_password_description": "Требовать пароль для доступа к объектам", "shared_links": "Публичные ссылки", "shared_links_description": "Делитесь фотографиями и видео по ссылке", "shared_photos_and_videos_count": "{assetCount, plural, other {# фото и видео.}}", "shared_with_me": "Доступные мне", - "shared_with_partner": "Совместно с {partner}", + "shared_with_partner": "Вы предоставили доступ пользователю {partner}", "sharing": "Общие", "sharing_enter_password": "Пожалуйста, введите пароль для просмотра этой страницы.", "sharing_page_album": "Общие альбомы", @@ -1830,7 +1876,7 @@ "sharing_silver_appbar_create_shared_album": "Создать общий альбом", "sharing_silver_appbar_share_partner": "Поделиться с партнёром", "shift_to_permanent_delete": "нажмите ⇧ чтобы удалить объект навсегда", - "show_album_options": "Показать параметры альбома", + "show_album_options": "Действия с альбомом", "show_albums": "Показать альбомы", "show_all_people": "Показать всех людей", "show_and_hide_people": "Показать и скрыть людей", @@ -1838,21 +1884,22 @@ "show_gallery": "Показать галерею", "show_hidden_people": "Показать скрытых людей", "show_in_timeline": "Показать на временной шкале", - "show_in_timeline_setting_description": "Показывайте фото и видео этого пользователя в своей ленте", + "show_in_timeline_setting_description": "Отображать фото и видео этого пользователя на своей временной шкале", "show_keyboard_shortcuts": "Показать сочетания клавиш", "show_metadata": "Показывать метаданные", "show_or_hide_info": "Показать или скрыть информацию", "show_password": "Показать пароль", - "show_person_options": "Показать опции персоны", + "show_person_options": "Действия с человеком", "show_progress_bar": "Показать Индикатор Выполнения", "show_search_options": "Показать параметры поиска", "show_shared_links": "Показать публичные ссылки", "show_slideshow_transition": "Показать слайд-шоу переход", "show_supporter_badge": "Значок поддержки", "show_supporter_badge_description": "Показать значок поддержки", + "show_text_search_menu": "Показать меню текстового поиска", "shuffle": "Перемешать", "sidebar": "Боковая панель", - "sidebar_display_description": "Показывать ссылку на представление в боковой панели", + "sidebar_display_description": "Отображать раздел на боковой панели", "sign_out": "Выход", "sign_up": "Зарегистрироваться", "size": "Размер", @@ -1880,13 +1927,14 @@ "stacktrace": "Трассировка стека", "start": "Старт", "start_date": "Дата начала", + "start_date_before_end_date": "Дата начала должна быть меньше даты окончания", "state": "Регион", "status": "Состояние", "stop_casting": "Остановить трансляцию", "stop_motion_photo": "Покадровая анимация", - "stop_photo_sharing": "Закрыть доступ партнёра к вашим фото?", - "stop_photo_sharing_description": "{partner} больше не сможет получить доступ к вашим фотографиям.", - "stop_sharing_photos_with_user": "Прекратить делиться своими фотографиями с этим пользователем", + "stop_photo_sharing": "Закрыть доступ партнёру?", + "stop_photo_sharing_description": "Пользователь {partner} больше не имеет доступа к вашим фотографиям.", + "stop_sharing_photos_with_user": "Прекратить делиться своими фото и видео с этим пользователем", "storage": "Хранилище", "storage_label": "Метка хранилища", "storage_quota": "Квота хранилища", @@ -1902,9 +1950,11 @@ "sync": "Синхр.", "sync_albums": "Синхронизировать альбомы", "sync_albums_manual_subtitle": "Синхронизировать все загруженные фото и видео в выбранные альбомы для резервного копирования", - "sync_local": "Синхронизировать локально", + "sync_local": "Локальная синхронизация", "sync_remote": "Синхронизация с сервером", - "sync_upload_album_setting_subtitle": "Создавайте и загружайте свои фотографии и видео в выбранные альбомы на сервер Immich", + "sync_status": "Статус синхронизации", + "sync_status_subtitle": "Просмотр и управление системой синхронизации", + "sync_upload_album_setting_subtitle": "Создавать на сервере такие же альбомы, как выбранные на устройстве, и загружать в них фото и видео", "tag": "Тег", "tag_assets": "Добавить теги", "tag_created": "Тег {tag} создан", @@ -1918,7 +1968,7 @@ "template": "Шаблон", "theme": "Тема", "theme_selection": "Выбор темы", - "theme_selection_description": "Автоматически устанавливать тему в зависимости от системных настроек вашего браузера", + "theme_selection_description": "Автоматически устанавливать светлую или тёмную тему в зависимости от настроек вашего браузера", "theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов", "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})", "theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.", @@ -1941,7 +1991,9 @@ "to_change_password": "Изменить пароль", "to_favorite": "Добавить в избранное", "to_login": "Вход", + "to_multi_select": "выбрать несколько", "to_parent": "Вернуться назад", + "to_select": "выбрать", "to_trash": "Корзина", "toggle_settings": "Переключение настроек", "total": "Всего", @@ -1961,6 +2013,7 @@ "trash_page_select_assets_btn": "Выбранные объекты", "trash_page_title": "Корзина ({count})", "trashed_items_will_be_permanently_deleted_after": "Объекты, хранящиеся в корзине более {days, plural, one {# дня} other {# дней}}, удаляются автоматически.", + "troubleshoot": "Диагностика", "type": "Тип", "unable_to_change_pin_code": "Ошибка при изменении PIN-кода", "unable_to_setup_pin_code": "Ошибка при создании PIN-кода", @@ -1991,6 +2044,7 @@ "unstacked_assets_count": "{count, plural, one {Разгруппирован # объект} many {Разгруппировано # объектов} other {Разгруппировано # объекта}}", "untagged": "Без тегов", "up_next": "Следующее", + "update_location_action_prompt": "Установить следующие координаты у выбранных объектов ({count} шт.):", "updated_at": "Обновлён", "updated_password": "Пароль изменён", "upload": "Загрузить", @@ -2018,7 +2072,7 @@ "user": "Пользователь", "user_has_been_deleted": "Этот пользователь был удалён.", "user_id": "ID пользователя", - "user_liked": "{user} отметил(а) {type, select, photo {это фото} video {это видео} asset {этот ресурс} other {этот альбом}}", + "user_liked": "Пользователю {user} нравится {type, select, photo {это фото} video {это видео} asset {этот объект} other {этот альбом}}", "user_pin_code_settings": "PIN-код", "user_pin_code_settings_description": "Настройка PIN-кода для доступа к личной папке", "user_privacy": "Конфиденциальность пользователя", @@ -2053,10 +2107,11 @@ "view_in_timeline": "Показать на временной шкале", "view_link": "Показать ссылку", "view_links": "Показать ссылки", - "view_name": "Посмотреть", + "view_name": "Вид", "view_next_asset": "Показать следующий объект", "view_previous_asset": "Показать предыдущий объект", "view_qr_code": "Посмотреть QR код", + "view_similar_photos": "Найти похожие фотографии", "view_stack": "Показать группу", "view_user": "Просмотреть пользователя", "viewer_remove_from_stack": "Убрать из группы", @@ -2075,5 +2130,6 @@ "yes": "Да", "you_dont_have_any_shared_links": "У вас нет публичных ссылок", "your_wifi_name": "Имя вашей Wi-Fi сети", - "zoom_image": "Приблизить" + "zoom_image": "Изменить масштаб", + "zoom_to_bounds": "Увеличить до границ" } diff --git a/i18n/sk.json b/i18n/sk.json index 22e6ad8afb..2fdd6ca07a 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -28,6 +28,7 @@ "add_to_album": "Pridať do albumu", "add_to_album_bottom_sheet_added": "Pridané do {album}", "add_to_album_bottom_sheet_already_exists": "Už je v {album}", + "add_to_album_bottom_sheet_some_local_assets": "Niektoré lokálne súbory nebolo možné pridať do albumu", "add_to_album_toggle": "Prepnúť výber pre {album}", "add_to_albums": "Pridať do albumov", "add_to_albums_count": "Pridať do albumov ({count})", @@ -123,6 +124,13 @@ "logging_enable_description": "Povoliť ukladanie záznamov", "logging_level_description": "Ak je povolené, akú úroveň záznamov použiť.", "logging_settings": "Ukladanie záznamov", + "machine_learning_availability_checks": "Kontroly dostupnosti", + "machine_learning_availability_checks_description": "Automaticky zistiť a uprednostniť dostupné servery strojového učenia", + "machine_learning_availability_checks_enabled": "Povoliť kontroly dostupnosti", + "machine_learning_availability_checks_interval": "Interval kontroly", + "machine_learning_availability_checks_interval_description": "Interval v milisekundách medzi kontrolami dostupnosti", + "machine_learning_availability_checks_timeout": "Časový limit požiadavky", + "machine_learning_availability_checks_timeout_description": "Časový limit v milisekundách pre kontroly dostupnosti", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "Názov modelu CLIP je uvedený tu. Pamätajte, že pri zmene modelu je nutné znovu spustiť úlohu 'Inteligentné vyhľadávanie' pre všetky obrázky.", "machine_learning_duplicate_detection": "Detekcia duplikátov", @@ -387,8 +395,6 @@ "admin_password": "Administrátorské heslo", "administration": "Administrácia", "advanced": "Pokročilé", - "advanced_settings_beta_timeline_subtitle": "Vyskúšajte prostredie novej aplikácie", - "advanced_settings_beta_timeline_title": "Beta verzia časovej osi", "advanced_settings_enable_alternate_media_filter_subtitle": "Túto možnosť použite na filtrovanie médií počas synchronizácie na základe alternatívnych kritérií. Túto možnosť vyskúšajte len vtedy, ak máte problémy s detekciou všetkých albumov v aplikácii.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNE] Použiť alternatívny filter synchronizácie albumu zariadenia", "advanced_settings_log_level_title": "Úroveň ukladania záznamov: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Uprednostniť vzdialené obrázky", "advanced_settings_proxy_headers_subtitle": "Určite hlavičky proxy servera, ktoré by mal Immich posielať s každou požiadavkou na sieť", "advanced_settings_proxy_headers_title": "Proxy hlavičky", + "advanced_settings_readonly_mode_subtitle": "Aktivuje režim iba na čítanie, v ktorom je možné fotografie iba prezerať, pričom funkcie ako výber viacerých obrázkov, zdieľanie, prenášanie a mazanie sú deaktivované. Aktivácia/deaktivácia režimu iba na čítanie prostredníctvom obrázku používateľa na hlavnej obrazovke", + "advanced_settings_readonly_mode_title": "Režim iba na čítanie", "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_sync_remote_deletions_subtitle": "Automaticky vymazať alebo obnoviť položku na tomto zariadení, keď sa táto akcia vykoná na webe", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Ste si istý, že chcete odstrániť používateľa {user}?", "album_search_not_found": "Neboli nájdené žiadne albumy zodpovedajúce vášmu hľadaniu", "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_summary": "Súhrn albumu", "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}", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Odhlásiť sa", "app_settings": "Nastavenia aplikácie", "appears_in": "Vyskytuje sa v", + "apply_count": "Použiť ({count, number})", "archive": "Archív", "archive_action_prompt": "{count} pridaných do archívu", "archive_or_unarchive_photo": "Archivácia alebo odarchivovanie fotografie", @@ -493,6 +503,8 @@ "asset_restored_successfully": "Položky boli úspešne obnovené", "asset_skipped": "Preskočené", "asset_skipped_in_trash": "V koši", + "asset_trashed": "Položka bola vyhodená", + "asset_troubleshoot": "Riešenie problémov s položkami", "asset_uploaded": "Nahrané", "asset_uploading": "Nahráva sa…", "asset_viewer_settings_subtitle": "Spravujte nastavenia prehliadača galérie", @@ -500,7 +512,7 @@ "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_albums_count": "{assetTotal, plural, one {Pridaná # položka} few {Pridané # položky} other {Pridaných # položiek}} do {albumTotal} albumov", + "assets_added_to_albums_count": "{assetTotal, plural, one {Pridaná # položka} few {Pridané # položky} other {Pridaných # položiek}} do {albumTotal, plural, one {# albumu} other {# albumov}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {položku} other {položiek}} nie je možné pridať do albumu", "assets_cannot_be_added_to_albums": "{count, plural, one {položka} few {položky} other {položiek}} nie je možné pridať do žiadneho albumu", "assets_count": "{count, plural, one {# položka} few {# položky} other {# položiek}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "Automatické prehrávanie prezentácie", "back": "Späť", "back_close_deselect": "Späť, zavrieť alebo zrušiť výber", + "background_backup_running_error": "V súčasnosti prebieha zálohovanie na pozadí, nie je možné spustiť ručné zálohovanie", "background_location_permission": "Povolenie na určenie polohy na pozadí", "background_location_permission_content": "Aby bolo možné prepínať siete pri spustení na pozadí, musí mať aplikácia Immich *vždy* presný prístup k polohe, aby mohla prečítať názov siete Wi-Fi", + "background_options": "Možnosti pozadia", "backup": "Zálohovanie", "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", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Vybrať albumy", "backup_album_selection_page_selection_info": "Informácie o výbere", "backup_album_selection_page_total_assets": "Celkový počet jedinečných súborov", + "backup_albums_sync": "Synchronizácia zálohovaných albumov", "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…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "Spravovať nastavenia odosielania na pozadí a v popredí", "backup_settings_subtitle": "Spravovať nastavenia nahrávania", "backward": "Dozadu", - "beta_sync": "Stav synchronizácie verzie Beta", - "beta_sync_subtitle": "Spravovať nový systém synchronizácie", "biometric_auth_enabled": "Biometrické overovanie je povolené", "biometric_locked_out": "Ste vymknutí z biometrického overovania", "biometric_no_options": "Nie sú k dispozícii žiadne biometrické možnosti", @@ -653,6 +666,8 @@ "change_pin_code": "Zmeniť PIN kód", "change_your_password": "Zmeniť heslo", "changed_visibility_successfully": "Viditeľnosť bola úspešne zmenená", + "charging": "Nabíja sa", + "charging_requirement_mobile_backup": "Zálohovanie na pozadí vyžaduje, aby bolo zariadenie nabíjané", "check_corrupt_asset_backup": "Skontrolovať, či nie sú poškodené zálohy položiek", "check_corrupt_asset_backup_button": "Vykonať kontrolu", "check_corrupt_asset_backup_description": "Spustiť túto kontrolu len cez Wi-Fi a po zálohovaní všetkých položiek. Tento postup môže trvať niekoľko minút.", @@ -739,6 +754,7 @@ "create_user": "Vytvoriť používateľa", "created": "Vytvorené", "created_at": "Vytvorené", + "creating_linked_albums": "Vytváranie prepojených albumov...", "crop": "Orezať", "curated_object_page_title": "Veci", "current_device": "Súčasné zariadenie", @@ -888,7 +904,9 @@ "error": "Chyba", "error_change_sort_album": "Nepodarilo sa zmeniť poradie albumu", "error_delete_face": "Chyba pri odstraňovaní tváre z položky", + "error_getting_places": "Chyba pri získavaní polôh", "error_loading_image": "Nepodarilo sa načítať obrázok", + "error_loading_partners": "Chyba pri načítaní partnerov: {error}", "error_saving_image": "Chyba: {error}", "error_tag_face_bounding_box": "Chyba pri označovaní tváre - nemožno získať súradnice ohraničujúceho poľa", "error_title": "Chyba - niečo sa pokazilo", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "Žiadne obľúbené médiá", "feature_photo_updated": "Hlavný obrázok bol aktualizovaný", "features": "Funkcie", + "features_in_development": "Funkcie vo vývoji", "features_setting_description": "Spravovať funkcie aplikácie", "file_name": "Názov súboru", "file_name_or_extension": "Názov alebo prípona súboru", @@ -1073,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Táto funkcia načítava externé zdroje zo spoločnosti Google, aby mohla fungovať.", "general": "Všeobecné", + "geolocation_instruction_location": "Kliknite na položku s GPS súradnicami, aby ste použili jej polohu, alebo vyberte polohu priamo z mapy", "get_help": "Získať pomoc", "get_wifiname_error": "Nepodarilo sa získať názov Wi-Fi siete. Uistite sa, že ste udelili potrebné oprávnenia a ste pripojení k sieti Wi-Fi", "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", + "gps": "GPS", + "gps_missing": "Žiadne GPS", "grant_permission": "Udeliť povolenie", "group_albums_by": "Zoskupiť albumy podľa...", "group_country": "Zoskupenie podľa krajiny", @@ -1214,6 +1236,7 @@ "local": "Lokálne", "local_asset_cast_failed": "Nie je možné preniesť médium, ktoré nie je nahrané na serveri", "local_assets": "Lokálne položky", + "local_media_summary": "Súhrn lokálnych médií", "local_network": "Miestna sieť", "local_network_sheet_info": "Pri použití zadanej siete Wi-Fi sa aplikácia pripojí k serveru prostredníctvom tejto URL adresy", "location_permission": "Povolenie na určenie polohy", @@ -1225,6 +1248,7 @@ "location_picker_longitude_hint": "Zadajte platnú zemepisnú dĺžku", "lock": "Zamknúť", "locked_folder": "Zamknutý priečinok", + "log_detail_title": "Podrobnosti o zázname", "log_out": "Odhlásiť sa", "log_out_all_devices": "Odhlásiť všetky zariadenia", "logged_in_as": "Prihlásený ako {user}", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "Aktualizácia hesla prebehla úspešne", "logout_all_device_confirmation": "Ste si istý, že sa chcete odhlásiť zo všetkých zariadení?", "logout_this_device_confirmation": "Ste si istý, že sa chcete odhlásiť z tohoto zariadenia?", + "logs": "Záznamy", "longitude": "Zemepisná dĺžka", "look": "Vzhľad", "loop_videos": "Opakovať videá", @@ -1262,6 +1287,7 @@ "main_branch_warning": "Používate vývojársku verziu; dôrazne odporúčame používať vydané verzie!", "main_menu": "Hlavná ponuka", "make": "Výrobca", + "manage_geolocation": "Spravovať polohu", "manage_shared_links": "Spravovať zdieľané odkazy", "manage_sharing_with_partners": "Spravovať zdieľanie s partnermi", "manage_the_app_settings": "Spravovať nastavenia aplikácie", @@ -1296,6 +1322,7 @@ "mark_as_read": "Označiť ako prečítané", "marked_all_as_read": "Označené všetko ako prečítané", "matches": "Zhody", + "matching_assets": "Vyhovujúce položky", "media_type": "Typ média", "memories": "Spomienky", "memories_all_caught_up": "Na dnes to je všetko", @@ -1336,6 +1363,7 @@ "name_or_nickname": "Meno alebo prezývka", "network_requirement_photos_upload": "Použiť mobilné dáta na zálohovanie fotografií", "network_requirement_videos_upload": "Použiť mobilné dáta na zálohovanie videí", + "network_requirements": "Požiadavky na sieť", "network_requirements_updated": "Požiadavky na sieť sa zmenili, obnovuje sa poradie zálohovania", "networking_settings": "Sieť", "networking_subtitle": "Spravovať nastavenia koncového bodu servera", @@ -1346,6 +1374,7 @@ "new_person": "Nová osoba", "new_pin_code": "Nový PIN kód", "new_pin_code_subtitle": "Toto je váš prvý prístup k zamknutému priečinku. Vytvorte si PIN kód na bezpečný prístup k tejto stránke", + "new_timeline": "Nová časová os", "new_user_created": "Nový používateľ vytvorený", "new_version_available": "JE DOSTUPNÁ NOVÁ VERZIA", "newest_first": "Najprv najnovšie", @@ -1359,20 +1388,25 @@ "no_assets_message": "KLIKNITE A NAHRAJTE SVOJU PRVÚ FOTKU", "no_assets_to_show": "Žiadne položky", "no_cast_devices_found": "Nenašli sa žiadne zariadenia na prenos", + "no_checksum_local": "Kontrola súčtu nie je k dispozícii – nie je možné načítať lokálne položky", + "no_checksum_remote": "Kontrola súčtu nie je k dispozícii – nie je možné načítať vzdialené položky", "no_duplicates_found": "Nenašli sa žiadne duplicity.", "no_exif_info_available": "Nie sú dostupné exif údaje", "no_explore_results_message": "Nahrajte viac fotiek na objavovanie vašej zbierky.", "no_favorites_message": "Pridajte si obľúbené, aby ste rýchlo našli svoje najlepšie obrázky a videá", "no_libraries_message": "Vytvorí externú knižnicu na prezeranie fotiek a videí", + "no_local_assets_found": "Neboli nájdené žiadne lokálne položky s touto kontrolnou sumou", "no_locked_photos_message": "Fotografie a videá v zamknutom priečinku sú skryté a nezobrazujú sa pri prehľadávaní alebo vyhľadávaní v knižnici.", "no_name": "Bez mena", "no_notifications": "Žiadne oznámenia", "no_people_found": "Nenašli sa žiadni vyhovujúci ľudia", "no_places": "Bez miesta", + "no_remote_assets_found": "Neboli nájdené žiadne vzdialené položky s touto kontrolnou sumou", "no_results": "Žiadne výsledky", "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", "no_uploads_in_progress": "Žiadne prebiehajúce nahrávanie", + "not_available": "Nedostupné", "not_in_any_album": "Nie je v žiadnom albume", "not_selected": "Nevybrané", "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", @@ -1407,6 +1441,8 @@ "open_the_search_filters": "Otvoriť vyhľadávacie filtre", "options": "Nastavenia", "or": "alebo", + "organize_into_albums": "Usporiadať do albumov", + "organize_into_albums_description": "Vložiť existujúce fotky do albumov podľa aktuálnych nastavení synchronizácie", "organize_your_library": "Usporiadajte svoju knižnicu", "original": "originál", "other": "Ostatné", @@ -1492,6 +1528,7 @@ "port": "Port", "preferences_settings_subtitle": "Spravovať predvoľby aplikácie", "preferences_settings_title": "Predvoľby", + "preparing": "Pripravuje sa", "preset": "Predvoľba", "preview": "Náhľad", "previous": "Predošlé", @@ -1508,6 +1545,7 @@ "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_readonly_mode": "Režim iba na čítanie je aktivovaný. Dlhým stlačením ikony obrázku používateľa režim opustíte.", "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}", @@ -1546,6 +1584,7 @@ "purchase_server_description_2": "Štatút podporovateľa", "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktový kľúč servera spravuje admin", + "query_asset_id": "ID požiadavky položky", "queue_status": "V poradí {count}/{total}", "rating": "Hodnotenie hviezdičkami", "rating_clear": "Vyčistiť hodnotenie", @@ -1553,6 +1592,9 @@ "rating_description": "Zobraziť EXIF hodnotenie v informačnom paneli", "reaction_options": "Možnosti reakcie", "read_changelog": "Prečítať zoznam zmien", + "readonly_mode_disabled": "Režim iba na čítanie je vypnutý", + "readonly_mode_enabled": "Režim iba na čítanie je zapnutý", + "ready_for_upload": "Pripravené na nahratie", "reassign": "Preradiť", "reassigned_assets_to_existing_person": "Opätovne {count, plural, one {priradená # položka} few {priradené # položky} other {priradených # položiek}} k {name, select, null {existujúcej osobe} other {{name}}}", "reassigned_assets_to_new_person": "Opätovne {count, plural, one {priradená # položka} few {priradené # položky} other {priradených # položiek}} novej osobe", @@ -1577,6 +1619,7 @@ "regenerating_thumbnails": "Pregenerovanie náhľadov", "remote": "Vzdialené", "remote_assets": "Vzdialené položky", + "remote_media_summary": "Súhrn vzdialených médií", "remove": "Odstrániť", "remove_assets_album_confirmation": "Naozaj chcete odstrániť {count, plural, one {# položku} few {# položky} other {# položiek}} z albumu?", "remove_assets_shared_link_confirmation": "Naozaj chcete odstrániť {count, plural, one {# položku} few {# položky} other {# položiek}} z tohoto zdieľaného odkazu?", @@ -1629,6 +1672,7 @@ "restore_user": "Navrátiť používateľa", "restored_asset": "Navrátená položka", "resume": "Pokračovať", + "resume_paused_jobs": "Pokračovať v {count, plural, one {# pozastavenej úlohe} other {# pozastavených úlohách}}", "retry_upload": "Zopakovať nahrávanie", "review_duplicates": "Preskúmať duplikáty", "review_large_files": "Skontrolovať veľké súbory", @@ -1700,7 +1744,7 @@ "search_tags": "Hľadať štítky...", "search_timezone": "Hľadať časovú zónu...", "search_type": "Typ hľadania", - "search_your_photos": "Hľadajte svoje fotky", + "search_your_photos": "Vyhľadávanie vo vašich fotografiách", "searching_locales": "Hľadám lokality...", "second": "Sekundy", "see_all_people": "Pozrieť všetky osoby", @@ -1722,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album", "selected": "Vybrané", "selected_count": "{count, plural, one {# vybraná} few {# vybrané} other {# vybraných}}", + "selected_gps_coordinates": "Vybrané GPS súradnice", "send_message": "Odoslať správu", "send_welcome_email": "Odoslať uvítací e-mail", "server_endpoint": "Koncový bod servera", @@ -1850,6 +1895,7 @@ "show_slideshow_transition": "Zobraziť prechody v prezentácii", "show_supporter_badge": "Odznak podporovateľa", "show_supporter_badge_description": "Zobraziť odznak podporovateľa", + "show_text_search_menu": "Zobraziť ponuku vyhľadávania textu", "shuffle": "Náhodné poradie", "sidebar": "Bočný panel", "sidebar_display_description": "Zobraziť odkaz na zobrazenie v bočnom paneli", @@ -1880,6 +1926,7 @@ "stacktrace": "Výpis zásobníku", "start": "Spustiť", "start_date": "Počiatočný dátum", + "start_date_before_end_date": "Dátum začiatku musí byť pred dátumom ukončenia", "state": "Štát", "status": "Stav", "stop_casting": "Zastaviť prenos", @@ -1904,6 +1951,8 @@ "sync_albums_manual_subtitle": "Synchronizujte všetky nahrané videá a fotografie s vybranými záložnými albumami", "sync_local": "Synchronizovať lokálne", "sync_remote": "Synchronizovať vzdialené", + "sync_status": "Stav synchronizácie", + "sync_status_subtitle": "Zobraziť a spravovať systém synchronizácie", "sync_upload_album_setting_subtitle": "Vytvárajte a nahrávajte svoje fotografie a videá do vybraných albumov na Immich", "tag": "Štítok", "tag_assets": "Pridať štítky", @@ -1941,7 +1990,9 @@ "to_change_password": "Zmeniť heslo", "to_favorite": "Obľúbiť", "to_login": "Prihlásiť", + "to_multi_select": "na viacnásobný výber", "to_parent": "Prejsť k nadradenému", + "to_select": "na výber", "to_trash": "Kôš", "toggle_settings": "Prepnúť nastavenie", "total": "Celkom", @@ -1961,6 +2012,7 @@ "trash_page_select_assets_btn": "Vybrať médiá", "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}}.", + "troubleshoot": "Riešenie problémov", "type": "Typ", "unable_to_change_pin_code": "Nie je možné zmeniť PIN kód", "unable_to_setup_pin_code": "Nie je možné nastaviť PIN kód", @@ -1991,6 +2043,7 @@ "unstacked_assets_count": "Zrušenie zoskupenia pre {count, plural, one {# položku} few {# položky} other {# položiek}}", "untagged": "Bez štítku", "up_next": "To je všetko", + "update_location_action_prompt": "Aktualizovať polohu {count} vybraných položiek pomocou:", "updated_at": "Aktualizované", "updated_password": "Heslo zmenené", "upload": "Nahrať", @@ -2057,6 +2110,7 @@ "view_next_asset": "Zobraziť nasledujúci súbor", "view_previous_asset": "Zobraziť predchádzajúci súbor", "view_qr_code": "Zobraziť QR kód", + "view_similar_photos": "Zobraziť podobné fotografie", "view_stack": "Zobraziť zoskupenie", "view_user": "Zobraziť používateľa", "viewer_remove_from_stack": "Odstrániť zo zoskupenia", @@ -2075,5 +2129,6 @@ "yes": "Áno", "you_dont_have_any_shared_links": "Nemáte žiadne zdielané odkazy", "your_wifi_name": "Váš názov siete Wi-Fi", - "zoom_image": "Priblížiť obrázok" + "zoom_image": "Priblížiť obrázok", + "zoom_to_bounds": "Zväčšiť na okraje" } diff --git a/i18n/sl.json b/i18n/sl.json index be6f4b3047..7a1aef509a 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -8,7 +8,7 @@ "actions": "Dejanja", "active": "Aktivno", "activity": "Aktivnost", - "activity_changed": "Aktivnost {enabled, select, true {omogočena} other {onemogočena}}", + "activity_changed": "Aktivnost je {enabled, select, true {omogočena} other {onemogočena}}", "add": "Dodaj", "add_a_description": "Dodaj opis", "add_a_location": "Dodaj lokacijo", @@ -28,6 +28,10 @@ "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_album_bottom_sheet_some_local_assets": "Nekaterih lokalnih sredstev ni bilo mogoče dodati v album", + "add_to_album_toggle": "Preklopi izbiro za {album}", + "add_to_albums": "Dodaj v albume", + "add_to_albums_count": "Dodaj v albume ({count})", "add_to_shared_album": "Dodaj k deljenemu albumu", "add_url": "Dodaj URL", "added_to_archive": "Dodano v arhiv", @@ -36,15 +40,15 @@ "admin": { "add_exclusion_pattern_description": "Dodajte vzorec izključitev. Globiranje z uporabo *, ** in ? je podprto. Če želite prezreti vse datoteke v katerem koli imeniku z imenom \"Raw\", uporabite \"**/Raw/**\". Če želite prezreti vse datoteke, ki se končajo na \".tif\", uporabite \"**/*.tif\". Če želite prezreti absolutno pot, uporabite \"/pot/za/ignoriranje/**\".", "admin_user": "Skrbniški uporabnik", - "asset_offline_description": "Sredstva zunanje knjižnice ni več mogoče najti na disku in je bilo premaknjeno v koš. Če je bila datoteka premaknjena znotraj knjižnice, preverite svojo časovnico za novo ustrezno sredstvo. Če želite obnoviti to sredstvo, zagotovite, da ima Immich dostop do spodnje poti datoteke, in skenirajte knjižnico.", + "asset_offline_description": "Tega sredstva zunanje knjižnice ni več mogoče najti na disku in je bilo premaknjeno v koš. Če je bila datoteka premaknjena znotraj knjižnice, preverite svojo časovnico za novo ustrezno sredstvo. Če želite obnoviti to sredstvo, zagotovite, da ima Immich dostop do spodnje poti datoteke, in skenirajte knjižnico.", "authentication_settings": "Nastavitve preverjanja pristnosti", "authentication_settings_description": "Upravljanje gesel, OAuth in drugih nastavitev preverjanja pristnosti", "authentication_settings_disable_all": "Ali zares želite onemogočiti vse prijavne metode? Prijava bo popolnoma onemogočena.", - "authentication_settings_reenable": "Ponovno omogoči z uporabo strežniškega ukaza.", + "authentication_settings_reenable": "Za ponovno omogočanje uporabite strežniški ukaz.", "background_task_job": "Opravila v ozadju", "backup_database": "Ustvari izpis baze podatkov", "backup_database_enable_description": "Omogoči izpise baze podatkov", - "backup_keep_last_amount": "Število prejšnjih odlagališč, ki jih je treba obdržati", + "backup_keep_last_amount": "Število prejšnjih izpisov baze podatkov, ki jih je treba obdržati", "backup_onboarding_1_description": "kopijo zunaj lokacije v oblaku ali na drugi fizični lokaciji.", "backup_onboarding_2_description": "lokalne kopije na različnih napravah. To vključuje glavne datoteke in lokalno varnostno kopijo teh datotek.", "backup_onboarding_3_description": "skupno število kopij vaših podatkov, vključno z izvirnimi datotekami. To vključuje 1 kopijo zunaj lokacije in 2 lokalni kopiji.", @@ -54,7 +58,7 @@ "backup_onboarding_title": "Varnostne kopije", "backup_settings": "Nastavitve izpisa baze podatkov", "backup_settings_description": "Upravljanje nastavitev izpisa podatkovne baze.", - "cleared_jobs": "Razčiščeno opravilo za: {job}", + "cleared_jobs": "Razčiščena opravila za: {job}", "config_set_by_file": "Konfiguracija je trenutno nastavljena s konfiguracijsko datoteko", "confirm_delete_library": "Ali ste prepričani, da želite izbrisati knjižnico {library}?", "confirm_delete_library_assets": "Ali ste prepričani, da želite izbrisati to knjižnico? To bo iz Immicha izbrisalo {count, plural, one {# vsebovani vir} two {# vsebovana vira} few {# vsebovane vire} other {vseh # vsebovanih virov}} in tega ni možno razveljaviti. Datoteke bodo ostale na disku.", @@ -69,10 +73,10 @@ "disable_login": "Onemogoči prijavo", "duplicate_detection_job_description": "Zaženite strojno učenje na sredstvih, da zaznate podobne slike. Zanaša se na Pametno Iskanje", "exclusion_pattern_description": "Vzorci izključitev vam omogočajo, da prezrete datoteke in mape pri skeniranju knjižnice. To je uporabno, če imate mape z datotekami, ki jih ne želite uvoziti, na primer datoteke RAW.", - "external_library_management": "Upravljanje zunanje knjižnice", + "external_library_management": "Upravljanje zunanjih knjižnic", "face_detection": "Zaznavanje obrazov", - "face_detection_description": "Zaznajte obraze v sredstvih s pomočjo strojnega učenja. Pri videoposnetkih se upošteva samo sličica. \"Vse\" (ponovno) obdela vsa sredstva. \"Manjkajoče\" postavi v čakalno vrsto sredstva, ki še niso bila obdelana. Zaznani obrazi bodo postavljeni v čakalno vrsto za prepoznavanje obrazov, ko bo zaznavanje obrazov končano, in jih bodo združili v obstoječe ali nove osebe.", - "facial_recognition_job_description": "Združi zaznane obraze v osebe. Ta korak se izvede po končanem zaznavanju obrazov. \"Vse\" (ponovno) združuje vse obraze. \"Manjkajoče\", doda v čakalno vrsto obraze, ki nimajo dodeljene osebe.", + "face_detection_description": "Zaznavanje obrazov v sredstvih z uporabo strojnega učenja. Pri videoposnetkih se upošteva samo sličica. »Osveži« (ponovno) obdela vsa sredstva. »Ponastavi« dodatno izbriše vse trenutne podatke o obrazih. »Manjkajoča« uvrsti sredstva, ki še niso bila obdelana, v čakalno vrsto. Zaznani obrazi bodo po končanem zaznavanju obrazov uvrščeni v čakalno vrsto za prepoznavanje obrazov, pri čemer bodo združeni v obstoječe ali nove osebe.", + "facial_recognition_job_description": "Združi zaznane obraze v osebe. Ta korak se izvede po končanem zaznavanju obrazov. »Ponastavi« (ponovno) združi vse obraze. »Manjkajoča« uvrsti obraze, ki jim ni dodeljena oseba, v čakalno vrsto.", "failed_job_command": "Za opravilo {job} ukaz {command} ni uspel", "force_delete_user_warning": "OPOZORILO: S tem boste takoj odstranili uporabnika in vsa sredstva. Tega ni mogoče razveljaviti in datotek ni mogoče obnoviti.", "image_format": "Format", @@ -99,12 +103,12 @@ "image_thumbnail_title": "Nastavitve sličic", "job_concurrency": "{job} sočasnost", "job_created": "Opravilo ustvarjeno", - "job_not_concurrency_safe": "To opravilo ni sočasno-varno.", + "job_not_concurrency_safe": "To delo ni varno za sočasnost.", "job_settings": "Nastavitve opravil", "job_settings_description": "Upravljaj sočasnost opravil", "job_status": "Status opravila", - "jobs_delayed": "{jobCount, plural, other {# zadržan}}", - "jobs_failed": "{jobCount, plural, other {# neuspešen}}", + "jobs_delayed": "{jobCount, plural, other {# zadržani}}", + "jobs_failed": "{jobCount, plural, other {# neuspešni}}", "library_created": "Ustvarjena knjižnica: {library}", "library_deleted": "Knjižnica izbrisana", "library_import_path_description": "Določi mapo za uvoz. Ta mapa in njene podmape bodo pregledane za slike in video posnetke.", @@ -120,6 +124,13 @@ "logging_enable_description": "Omogoči dnevnik", "logging_level_description": "Nivo dnevnika, ko je le-ta omogočen.", "logging_settings": "Dnevnik", + "machine_learning_availability_checks": "Preverjanja razpoložljivosti", + "machine_learning_availability_checks_description": "Samodejno zaznavanje in dajanje prednosti razpoložljivim strežnikom strojnega učenja", + "machine_learning_availability_checks_enabled": "Omogoči preverjanja razpoložljivosti", + "machine_learning_availability_checks_interval": "Interval preverjanja", + "machine_learning_availability_checks_interval_description": "Interval v milisekundah med preverjanji razpoložljivosti", + "machine_learning_availability_checks_timeout": "Zahteva za časovno omejitev", + "machine_learning_availability_checks_timeout_description": "Časovna omejitev v milisekundah za preverjanje razpoložljivosti", "machine_learning_clip_model": "model CLIP", "machine_learning_clip_model_description": "Ime CLIP modela iz seznama tukaj. Vedite, da boste morali po menjavi modela ponovno zagnati opravilo za 'Pametno iskanje' za vse slike.", "machine_learning_duplicate_detection": "Zaznavanje dvojnikov", @@ -128,7 +139,7 @@ "machine_learning_duplicate_detection_setting_description": "Za iskanje verjetnih dvojnikov uporabite vdelave CLIP", "machine_learning_enabled": "Omogoči strojno učenje", "machine_learning_enabled_description": "Če je onemogočeno, bodo vse funkcije strojnega učenja onemogočene ne glede na spodnje nastavitve.", - "machine_learning_facial_recognition": "Zaznavanje obrazov", + "machine_learning_facial_recognition": "Prepoznavanje obrazov", "machine_learning_facial_recognition_description": "Zaznavanje, prepoznavanje in združevanje obrazov na slikah", "machine_learning_facial_recognition_model": "Model za prepoznavanje obraza", "machine_learning_facial_recognition_model_description": "Modeli so navedeni v padajočem vrstnem redu glede na velikost. Večji modeli so počasnejši in uporabljajo več pomnilnika, vendar dajejo boljše rezultate. Upoštevajte, da morate po spremembi modela znova zagnati opravilo zaznavanja obrazov za vse slike.", @@ -141,7 +152,7 @@ "machine_learning_min_detection_score": "Najmanjši rezultat zaznavanja", "machine_learning_min_detection_score_description": "Najmanjši rezultat zaupanja za zaznavanje obraza od 0-1. Nižje vrednosti bodo zaznale več obrazov, vendar lahko povzročijo lažne pozitivne rezultate.", "machine_learning_min_recognized_faces": "Najmanjše število prepoznanih obrazov", - "machine_learning_min_recognized_faces_description": "Najmanjše število prepoznanih obrazov za osebo, ki se ustvari. Če to povečate, postane prepoznavanje obraza natančnejše na račun večje možnosti, da obraz ni dodeljen osebi.", + "machine_learning_min_recognized_faces_description": "Najmanjše število prepoznanih obrazov za osebo, da se ustvari. Če to povečate, postane prepoznavanje obraza natančnejše na račun večje možnosti, da obraz ni dodeljen osebi.", "machine_learning_settings": "Nastavitve strojnega učenja", "machine_learning_settings_description": "Upravljajte funkcije in nastavitve strojnega učenja", "machine_learning_smart_search": "Pametno iskanje", @@ -173,41 +184,41 @@ "metadata_settings": "Nastavitve metapodatkov", "metadata_settings_description": "Upravljanje nastavitev metapodatkov", "migration_job": "Migracija", - "migration_job_description": "Preselite sličice za sredstva in obraze v najnovejšo strukturo map", + "migration_job_description": "Prenesite sličice za sredstva in obraze v najnovejšo strukturo map", "nightly_tasks_cluster_faces_setting_description": "Zaženi prepoznavanje obrazov na novo zaznanih obrazih", "nightly_tasks_cluster_new_faces_setting": "Združite nove obraze", "nightly_tasks_database_cleanup_setting": "Naloge čiščenja baze podatkov", "nightly_tasks_database_cleanup_setting_description": "Očistite stare, potekle podatke iz baze podatkov", - "nightly_tasks_generate_memories_setting": "Ustvarjajte spomine", - "nightly_tasks_generate_memories_setting_description": "Ustvarite nove spomine iz sredstev", + "nightly_tasks_generate_memories_setting": "Ustvari spomine", + "nightly_tasks_generate_memories_setting_description": "Ustvari nove spomine iz sredstev", "nightly_tasks_missing_thumbnails_setting": "Ustvari manjkajoče sličice", "nightly_tasks_missing_thumbnails_setting_description": "Sredstva brez sličic postavite v čakalno vrsto za ustvarjanje sličic", "nightly_tasks_settings": "Nastavitve nočnih opravil", "nightly_tasks_settings_description": "Upravljajte nočne naloge", "nightly_tasks_start_time_setting": "Začetni čas", "nightly_tasks_start_time_setting_description": "Čas, ko strežnik začne izvajati nočne naloge", - "nightly_tasks_sync_quota_usage_setting": "Poraba kvote za sinhronizacijo", + "nightly_tasks_sync_quota_usage_setting": "Posodobi kvoto porabljenega prostora", "nightly_tasks_sync_quota_usage_setting_description": "Posodobi kvoto shrambe uporabnikov glede na trenutno uporabo", "no_paths_added": "Ni dodanih poti", - "no_pattern_added": "Brez dodanega vzorca", + "no_pattern_added": "Nobenega dodanega vzorca", "note_apply_storage_label_previous_assets": "Opomba: Če želite oznako za shranjevanje uporabiti za predhodno naložena sredstva, zaženite", "note_cannot_be_changed_later": "OPOMBA: Tega pozneje ni mogoče spremeniti!", - "notification_email_from_address": "Iz naslova", - "notification_email_from_address_description": "E-poštni naslov pošiljatelja, na primer: \"Immich Photo Server \". Uporabite naslov, s katerega lahko pošiljate e-pošto.", + "notification_email_from_address": "Od naslova", + "notification_email_from_address_description": "Pošiljateljev e-poštni naslov, na primer: \"Immich Photo Server \". Uporabite naslov, s katerega lahko pošiljate e-pošto.", "notification_email_host_description": "Gostitelj e-poštnega strežnika (npr. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Prezri napake potrdil", "notification_email_ignore_certificate_errors_description": "Prezri napake pri preverjanju potrdila TLS (ni priporočljivo)", "notification_email_password_description": "Geslo za uporabo pri preverjanju pristnosti z e-poštnim strežnikom", "notification_email_port_description": "Vrata e-poštnega strežnika (npr. 25, 465 ali 587)", - "notification_email_sent_test_email_button": "Pošljite testno e-pošto in shranite", + "notification_email_sent_test_email_button": "Pošljite testno e-pošto in shrani", "notification_email_setting_description": "Nastavitve za pošiljanje e-poštnih obvestil", "notification_email_test_email": "Pošlji testno e-pošto", - "notification_email_test_email_failed": "Pošiljanje testnega e-poštnega sporočila ni uspelo, preverite svoje vrednosti", + "notification_email_test_email_failed": "Pošiljanje testnega e-poštnega sporočila ni uspelo, preverite svoje podatke", "notification_email_test_email_sent": "Testno e-poštno sporočilo je bilo poslano na {email}. Prosimo, preverite svoj nabiralnik.", "notification_email_username_description": "Uporabniško ime za uporabo pri preverjanju pristnosti z e-poštnim strežnikom", "notification_enable_email_notifications": "Omogoči e-poštna obvestila", "notification_settings": "Nastavitve obvestil", - "notification_settings_description": "Upravljajte nastavitve obvestil, vključno z e-pošto", + "notification_settings_description": "Upravljaj z nastavitvami obvestil, vključno z e-pošto", "oauth_auto_launch": "Samodejni zagon", "oauth_auto_launch_description": "Samodejno zaženite tok prijave OAuth, ko obiščete stran za prijavo", "oauth_auto_register": "Samodejna registracija", @@ -218,7 +229,7 @@ "oauth_mobile_redirect_uri": "Mobilni preusmeritveni URI", "oauth_mobile_redirect_uri_override": "Preglasitev URI preusmeritve za mobilne naprave", "oauth_mobile_redirect_uri_override_description": "Omogoči, ko ponudnik OAuth ne dovoli mobilnega URI-ja, kot je ''{callback}''", - "oauth_role_claim": "Zahteva vloge", + "oauth_role_claim": "Zahteva za vlogo", "oauth_role_claim_description": "Samodejno dodeli skrbniški dostop na podlagi prisotnosti tega zahtevka. Zahtevek ima lahko »uporabnik« ali »skrbnik«.", "oauth_settings": "OAuth", "oauth_settings_description": "Upravljanje nastavitev prijave OAuth", @@ -238,13 +249,13 @@ "person_cleanup_job": "Čiščenje osebe", "quota_size_gib": "Velikost kvote (GiB)", "refreshing_all_libraries": "Osveževanje vseh knjižnic", - "registration": "Administratorska registracija", + "registration": "Registracija administratorja", "registration_description": "Ker ste prvi uporabnik v sistemu, boste dodeljeni kot skrbnik in ste odgovorni za skrbniška opravila, dodatne uporabnike pa boste ustvarili sami.", "require_password_change_on_login": "Od uporabnika zahtevajte spremembo gesla ob prvi prijavi", "reset_settings_to_default": "Ponastavi nastavitve na privzete", "reset_settings_to_recent_saved": "Ponastavite nastavitve na nedavno shranjene nastavitve", "scanning_library": "Pregledovanje knjižnice", - "search_jobs": "Iskanje opravil…", + "search_jobs": "Išči opravila…", "send_welcome_email": "Pošlji pozdravno e-pošto", "server_external_domain_settings": "Zunanja domena", "server_external_domain_settings_description": "Domena za javne skupne povezave, vključno s http(s)://", @@ -253,7 +264,7 @@ "server_settings": "Nastavitve strežnika", "server_settings_description": "Upravljanje nastavitev strežnika", "server_welcome_message": "Pozdravno sporočilo", - "server_welcome_message_description": "Sporočilo, ki se prikaže na strani za prijavo.", + "server_welcome_message_description": "Sporočilo prikazano na prijavni strani.", "sidecar_job": "Stranski metapodatki", "sidecar_job_description": "Odkrijte ali sinhronizirajte stranske metapodatke iz datotečnega sistema", "slideshow_duration_description": "Število sekund za prikaz posamezne slike", @@ -261,7 +272,7 @@ "storage_template_date_time_description": "Časovni žig ustvarjanja sredstva se uporablja za informacije o datumu in času", "storage_template_date_time_sample": "Vzorec časa {date}", "storage_template_enable_description": "Omogoči mehanizem predloge za shranjevanje", - "storage_template_hash_verification_enabled": "Preverjanje zgoščevanja je omogočeno", + "storage_template_hash_verification_enabled": "Omogočeno preverjanje zgoščene vrednosti", "storage_template_hash_verification_enabled_description": "Omogoči preverjanje zgoščene vrednosti, tega ne onemogočite, razen če niste prepričani o posledicah", "storage_template_migration": "Selitev predloge za shranjevanje", "storage_template_migration_description": "Uporabi trenutno {template} za predhodno naložena sredstva", @@ -280,7 +291,7 @@ "template_email_invite_album": "Predloga povabila v album", "template_email_preview": "Predogled", "template_email_settings": "E-poštne predloge", - "template_email_update_album": "Predloga posodobitve albuma", + "template_email_update_album": "Posodobi predlogo albuma", "template_email_welcome": "Predloga pozdravnega e-poštnega sporočila", "template_settings": "Predloge obvestil", "template_settings_description": "Upravljanje predlog po meri za obvestila", @@ -296,14 +307,14 @@ "transcoding_acceleration_qsv": "Hitra sinhronizacija (zahteva procesor Intel 7. generacije ali novejši)", "transcoding_acceleration_rkmpp": "RKMPP (samo na Rockchip SOC)", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "Sprejeti zvočni kodeki", + "transcoding_accepted_audio_codecs": "Dovoljeni zvočni kodeki", "transcoding_accepted_audio_codecs_description": "Izberite, katerih zvočnih kodekov ni treba prekodirati. Uporablja se samo za določene politike prekodiranja.", "transcoding_accepted_containers": "Sprejeti zabojniki", "transcoding_accepted_containers_description": "Izberite, katerih formatov zabojnika ni treba ponovno muksirati v MP4. Uporablja se samo za določene politike prekodiranja.", "transcoding_accepted_video_codecs": "Podprti video kodeki", "transcoding_accepted_video_codecs_description": "Izberite, katerih video kodekov ni treba prekodirati. Uporablja se samo za določene politike prekodiranja.", - "transcoding_advanced_options_description": "Možnosti večini uporabnikov ne bi bilo treba spreminjati", - "transcoding_audio_codec": "Avdio kodek", + "transcoding_advanced_options_description": "Možnosti, ki jih večini uporabnikov ne treba spreminjati", + "transcoding_audio_codec": "Zvočni kodek", "transcoding_audio_codec_description": "Opus je najbolj kakovostna možnost, vendar ima slabšo združljivost s starimi napravami ali programsko opremo.", "transcoding_bitrate_description": "Videoposnetki, ki presegajo največjo bitno hitrost ali niso v sprejemljivem formatu", "transcoding_codecs_learn_more": "Če želite izvedeti več o tukaj uporabljeni terminologiji, glejte dokumentacijo FFmpeg za kodek H.264, kodek HEVC in VP9 kodek.", @@ -384,8 +395,6 @@ "admin_password": "Skrbniško geslo", "administration": "Administracija", "advanced": "Napredno", - "advanced_settings_beta_timeline_subtitle": "Preizkusite novo izkušnjo aplikacije", - "advanced_settings_beta_timeline_title": "Časovnica beta različice", "advanced_settings_enable_alternate_media_filter_subtitle": "Uporabite to možnost za filtriranje medijev med sinhronizacijo na podlagi alternativnih meril. To poskusite le, če imate težave z aplikacijo, ki zaznava vse albume.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Uporabite alternativni filter za sinhronizacijo albuma v napravi", "advanced_settings_log_level_title": "Nivo dnevnika: {level}", @@ -393,6 +402,8 @@ "advanced_settings_prefer_remote_title": "Uporabi raje oddaljene slike", "advanced_settings_proxy_headers_subtitle": "Določi proxy glavo, ki jo naj Immich pošlje ob vsaki mrežni zahtevi", "advanced_settings_proxy_headers_title": "Proxy glave", + "advanced_settings_readonly_mode_subtitle": "Omogoči način samo za branje, kjer si je mogoče fotografije samo ogledati, funkcije, kot so izbiranje več slik, deljenje, predvajanje in brisanje, so onemogočene. Omogoči/onemogoči način samo za branje prek uporabniškega avatarja na glavnem zaslonu", + "advanced_settings_readonly_mode_title": "Način samo za branje", "advanced_settings_self_signed_ssl_subtitle": "Preskoči preverjanje potrdila SSL za končno točko strežnika. Zahtevano za samopodpisana potrdila.", "advanced_settings_self_signed_ssl_title": "Dovoli samopodpisana SSL potrdila", "advanced_settings_sync_remote_deletions_subtitle": "Samodejno izbriši ali obnovi sredstvo v tej napravi, ko je to dejanje izvedeno v spletu", @@ -420,6 +431,7 @@ "album_remove_user_confirmation": "Ali ste prepričani, da želite odstraniti {user}?", "album_search_not_found": "Ni najdenih albumov, ki bi ustrezali vašemu iskanju", "album_share_no_users": "Videti je, da ste ta album dali v skupno rabo z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi ga lahko delili.", + "album_summary": "Povzetek albuma", "album_updated": "Album posodobljen", "album_updated_setting_description": "Prejmite e-poštno obvestilo, ko ima album v skupni rabi nova sredstva", "album_user_left": "Zapustil {album}", @@ -458,6 +470,7 @@ "app_bar_signout_dialog_title": "Odjava", "app_settings": "Nastavitve aplikacije", "appears_in": "Pojavi se v", + "apply_count": "Uporabi ({count, number})", "archive": "Arhiv", "archive_action_prompt": "v arhiv je dodanih {count}", "archive_or_unarchive_photo": "Arhivirajte ali odstranite fotografijo iz arhiva", @@ -490,6 +503,8 @@ "asset_restored_successfully": "Sredstvo uspešno obnovljeno", "asset_skipped": "Preskočeno", "asset_skipped_in_trash": "V smetnjak", + "asset_trashed": "Sredstvo je bilo premaknjeno v koš", + "asset_troubleshoot": "Odpravljanje težav s sredstvi", "asset_uploaded": "Naloženo", "asset_uploading": "Nalaganje…", "asset_viewer_settings_subtitle": "Upravljaj nastavitve pregledovalnika galerije", @@ -497,7 +512,9 @@ "assets": "Sredstva", "assets_added_count": "Dodano{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_added_to_album_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v album", + "assets_added_to_albums_count": "Dodano {assetTotal, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v {albumTotal, plural, one {# album} two {# albuma} few {# albume} other {# albumov}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Sredstvo} two {Sredstvi} few {Sredstva} other {Sredstev}} ni mogoče dodati v album", + "assets_cannot_be_added_to_albums": "{count, plural, one {Sredstvo} two {Sredstvi} few {Sredstva} other {Sredstev}} ni mogoče dodati v noben album", "assets_count": "{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_deleted_permanently": "trajno izrisana sredstva {count}", "assets_deleted_permanently_from_server": "trajno izbrisana sredstva iz strežnika Immich {count}", @@ -514,14 +531,17 @@ "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", + "assets_were_part_of_albums_count": "{count, plural, one {Sredstvo je} two {Sredstvi sta} few {Sredstva so} other {Sredstev je}} že del albumov", "authorized_devices": "Pooblaščene naprave", "automatic_endpoint_switching_subtitle": "Povežite se lokalno prek določenega omrežja Wi-Fi, ko je na voljo, in uporabite druge povezave drugje", "automatic_endpoint_switching_title": "Samodejno preklapljanje URL-jev", "autoplay_slideshow": "Samodejno predvajanje diaprojekcije", "back": "Nazaj", "back_close_deselect": "Nazaj, zaprite ali prekličite izbiro", + "background_backup_running_error": "Varnostno kopiranje v ozadju se trenutno izvaja, ročnega varnostnega kopiranja ni mogoče zagnati", "background_location_permission": "Dovoljenje za iskanje lokacije v ozadju", "background_location_permission_content": "Ko deluje v ozadju mora imeti Immich za zamenjavo omrežij, *vedno* dostop do natančne lokacije, da lahko aplikacija prebere ime omrežja Wi-Fi", + "background_options": "Možnosti ozadja", "backup": "Varnostna kopija", "backup_album_selection_page_albums_device": "Albumi v napravi ({count})", "backup_album_selection_page_albums_tap": "Tapnite za vključitev, dvakrat tapnite za izključitev", @@ -529,6 +549,7 @@ "backup_album_selection_page_select_albums": "Izberi albume", "backup_album_selection_page_selection_info": "Informacije o izbiri", "backup_album_selection_page_total_assets": "Skupaj unikatnih sredstev", + "backup_albums_sync": "Sinhronizacija varnostnih kopij albumov", "backup_all": "Vse", "backup_background_service_backup_failed_message": "Varnostno kopiranje sredstev ni uspelo. Ponovno poskušam…", "backup_background_service_connection_failed_message": "Povezava s strežnikom ni uspela. Ponovno poskušam…", @@ -588,8 +609,6 @@ "backup_setting_subtitle": "Upravljaj nastavitve nalaganja v ozadju in ospredju", "backup_settings_subtitle": "Upravljanje nastavitev nalaganja", "backward": "Nazaj", - "beta_sync": "Stanje sinhronizacije beta različice", - "beta_sync_subtitle": "Upravljanje novega sistema sinhronizacije", "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", @@ -647,6 +666,8 @@ "change_pin_code": "Spremeni PIN kodo", "change_your_password": "Spremenite geslo", "changed_visibility_successfully": "Uspešno spremenjena vidnost", + "charging": "Polnjenje", + "charging_requirement_mobile_backup": "Za varnostno kopiranje v ozadju je potrebno polnjenje naprave", "check_corrupt_asset_backup": "Preverite poškodovane varnostne kopije sredstev", "check_corrupt_asset_backup_button": "Izvedi preverjanje", "check_corrupt_asset_backup_description": "To preverjanje zaženite samo prek omrežja Wi-Fi in potem, ko so vsa sredstva varnostno kopirana. Postopek lahko traja nekaj minut.", @@ -733,6 +754,7 @@ "create_user": "Ustvari uporabnika", "created": "Ustvarjeno", "created_at": "Ustvarjeno", + "creating_linked_albums": "Ustvarjanje povezanih albumov ...", "crop": "Obrezovanje", "curated_object_page_title": "Stvari", "current_device": "Trenutna naprava", @@ -882,7 +904,9 @@ "error": "Napaka", "error_change_sort_album": "Vrstnega reda albuma ni bilo mogoče spremeniti", "error_delete_face": "Napaka pri brisanju obraza iz sredstva", + "error_getting_places": "Napaka pri pridobivanju mest", "error_loading_image": "Napaka pri nalaganju slike", + "error_loading_partners": "Napaka pri nalaganju partnerjev: {error}", "error_saving_image": "Napaka: {error}", "error_tag_face_bounding_box": "Napaka pri označevanju obraza - ni mogoče pridobiti koordinat omejevalnega okvirja", "error_title": "Napaka - nekaj je šlo narobe", @@ -1047,6 +1071,7 @@ "favorites_page_no_favorites": "Ni priljubljenih sredstev", "feature_photo_updated": "Funkcijska fotografija je posodobljena", "features": "Funkcije", + "features_in_development": "Funkcije v razvoju", "features_setting_description": "Upravljaj funkcije aplikacije", "file_name": "Ime datoteke", "file_name_or_extension": "Ime ali končnica datoteke", @@ -1056,6 +1081,7 @@ "filter_people": "Filtriraj ljudi", "filter_places": "Filtriraj kraje", "find_them_fast": "Z iskanjem jih hitro poiščite po imenu", + "first": "Prvi", "fix_incorrect_match": "Popravi napačno ujemanje", "folder": "Mapa", "folder_not_found": "Ne najdem mape", @@ -1066,12 +1092,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ta funkcija za delovanje nalaga zunanje vire iz Googla.", "general": "Splošno", + "geolocation_instruction_location": "Kliknite na sredstvo z GPS koordinatami, da uporabite njegovo lokacijo, ali pa izberite lokacijo neposredno na zemljevidu", "get_help": "Poiščite pomoč", "get_wifiname_error": "Imena Wi-Fi ni bilo mogoče dobiti. Prepričajte se, da ste podelili potrebna dovoljenja in ste povezani v omrežje Wi-Fi", "getting_started": "Začetek", "go_back": "Pojdi nazaj", "go_to_folder": "Pojdi na mapo", "go_to_search": "Pojdi na iskanje", + "gps": "GPS", + "gps_missing": "Brez GPS-a", "grant_permission": "Podeli dovoljenje", "group_albums_by": "Združi albume po ...", "group_country": "Združi po državah", @@ -1177,6 +1206,7 @@ "language_search_hint": "Iskanje jezikov...", "language_setting_description": "Izberite želeni jezik", "large_files": "Velike datoteke", + "last": "Zadnji", "last_seen": "Nazadnje viden", "latest_version": "Najnovejša različica", "latitude": "Zemljepisna širina", @@ -1206,6 +1236,7 @@ "local": "Lokalno", "local_asset_cast_failed": "Sredstva, ki niso naložena na strežnik, ni mogoče predvajati", "local_assets": "Lokalna sredstva", + "local_media_summary": "Povzetek lokalnih medijev", "local_network": "Lokalno omrežje", "local_network_sheet_info": "Aplikacija se bo povezala s strežnikom prek tega URL-ja, ko bo uporabljala navedeno omrežje Wi-Fi", "location_permission": "Dovoljenje za lokacijo", @@ -1217,6 +1248,7 @@ "location_picker_longitude_hint": "Tukaj vnesi svojo zemljepisno dolžino", "lock": "Zaklepanje", "locked_folder": "Zaklenjena mapa", + "log_detail_title": "Podrobnosti dnevnika", "log_out": "Odjava", "log_out_all_devices": "Odjava vseh naprav", "logged_in_as": "Prijavljen kot {user}", @@ -1247,6 +1279,7 @@ "login_password_changed_success": "Geslo je bilo uspešno posodobljeno", "logout_all_device_confirmation": "Ali ste prepričani, da želite odjaviti vse naprave?", "logout_this_device_confirmation": "Ali ste prepričani, da se želite odjaviti iz te naprave?", + "logs": "Dnevniki", "longitude": "Zemljepisna dolžina", "look": "Izgled", "loop_videos": "Zanka videoposnetkov", @@ -1254,6 +1287,7 @@ "main_branch_warning": "Uporabljate razvojno različico; močno priporočamo uporabo izdajne različice!", "main_menu": "Glavni meni", "make": "Izdelava", + "manage_geolocation": "Upravljanje lokacije", "manage_shared_links": "Upravljanje povezav v skupni rabi", "manage_sharing_with_partners": "Upravljajte skupno rabo s partnerji", "manage_the_app_settings": "Upravljajte nastavitve aplikacije", @@ -1288,6 +1322,7 @@ "mark_as_read": "Označi kot prebrano", "marked_all_as_read": "Označeno vse kot prebrano", "matches": "Ujemanja", + "matching_assets": "Ujemajoča se sredstva", "media_type": "Vrsta medija", "memories": "Spomini", "memories_all_caught_up": "Vse dohiteno", @@ -1328,6 +1363,7 @@ "name_or_nickname": "Ime ali vzdevek", "network_requirement_photos_upload": "Uporaba mobilnih podatkov za varnostno kopiranje fotografij", "network_requirement_videos_upload": "Uporaba mobilnih podatkov za varnostno kopiranje videoposnetkov", + "network_requirements": "Omrežne zahteve", "network_requirements_updated": "Omrežne zahteve so se spremenile, ponastavitev čakalne vrste za varnostno kopiranje", "networking_settings": "Omrežje", "networking_subtitle": "Upravljaj nastavitve končne točke strežnika", @@ -1338,6 +1374,7 @@ "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_timeline": "Nova časovnica", "new_user_created": "Nov uporabnik ustvarjen", "new_version_available": "NA VOLJO JE NOVA RAZLIČICA", "newest_first": "Najprej najnovejše", @@ -1351,20 +1388,25 @@ "no_assets_message": "KLIKNITE ZA NALOŽITEV SVOJE PRVE FOTOGRAFIJE", "no_assets_to_show": "Ni sredstev za prikaz", "no_cast_devices_found": "Naprav za predvajanje ni bilo mogoče najti", + "no_checksum_local": "Kontrolna vsota ni na voljo – lokalnih sredstev ni mogoče pridobiti", + "no_checksum_remote": "Kontrolna vsota ni na voljo – oddaljenega sredstva ni mogoče pridobiti", "no_duplicates_found": "Najden ni bil noben dvojnik.", "no_exif_info_available": "Podatki o exif niso na voljo", "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_local_assets_found": "S to kontrolno vsoto ni bilo najdenih lokalnih sredstev", "no_locked_photos_message": "Fotografije in videoposnetki v zaklenjeni mapi so skriti in se ne bodo prikazali med brskanjem ali iskanjem po knjižnici.", "no_name": "Brez imena", "no_notifications": "Ni obvestil", "no_people_found": "Ni najdenih ustreznih oseb", "no_places": "Ni krajev", + "no_remote_assets_found": "S to kontrolno vsoto ni bilo najdenih oddaljenih sredstev", "no_results": "Brez rezultatov", "no_results_description": "Poskusite s sinonimom ali bolj splošno ključno besedo", "no_shared_albums_message": "Ustvarite album za skupno rabo fotografij in videoposnetkov z osebami v vašem omrežju", "no_uploads_in_progress": "Ni nalaganj v teku", + "not_available": "Ni na voljo", "not_in_any_album": "Ni v nobenem albumu", "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", @@ -1399,6 +1441,8 @@ "open_the_search_filters": "Odpri iskalne filtre", "options": "Možnosti", "or": "ali", + "organize_into_albums": "Organiziraj v albume", + "organize_into_albums_description": "Dodaj obstoječe fotografije v albume z uporabo trenutnih nastavitev sinhronizacije", "organize_your_library": "Organiziraj svojo knjižnico", "original": "izvirnik", "other": "drugo", @@ -1484,6 +1528,7 @@ "port": "Vrata", "preferences_settings_subtitle": "Upravljaj nastavitve aplikacije", "preferences_settings_title": "Nastavitve", + "preparing": "Priprava", "preset": "Prednastavitev", "preview": "Predogled", "previous": "Prejšnj-a/-i", @@ -1500,6 +1545,7 @@ "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_readonly_mode": "Način samo za branje je omogočen. Za izhod dolgo pritisnite ikono uporabniškega avatarja.", "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}", @@ -1538,6 +1584,7 @@ "purchase_server_description_2": "Status podpornika", "purchase_server_title": "Strežnik", "purchase_settings_server_activated": "Ključ izdelka strežnika upravlja skrbnik", + "query_asset_id": "ID sredstva poizvedbe", "queue_status": "Čakalna vrsta {count}/{total}", "rating": "Ocena z zvezdicami", "rating_clear": "Počisti oceno", @@ -1545,6 +1592,9 @@ "rating_description": "Prikažite oceno EXIF v informacijski plošči", "reaction_options": "Možnosti reakcije", "read_changelog": "Preberi dnevnik sprememb", + "readonly_mode_disabled": "Način samo za branje je onemogočen", + "readonly_mode_enabled": "Način samo za branje je omogočen", + "ready_for_upload": "Pripravljeno za nalaganje", "reassign": "Prerazporedi", "reassigned_assets_to_existing_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za {name, select, null {an existing person} other {{name}}}", "reassigned_assets_to_new_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za novo osebo", @@ -1569,6 +1619,7 @@ "regenerating_thumbnails": "Obnavljanje sličic", "remote": "Oddaljeno", "remote_assets": "Oddaljena sredstva", + "remote_media_summary": "Povzetek oddaljenih medijev", "remove": "Odstrani", "remove_assets_album_confirmation": "Ali ste prepričani, da želite odstraniti {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} iz albuma?", "remove_assets_shared_link_confirmation": "Ali ste prepričani, da želite odstraniti {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} iz te skupne povezave?", @@ -1621,6 +1672,7 @@ "restore_user": "Obnovi uporabnika", "restored_asset": "Obnovljeno sredstvo", "resume": "Nadaljuj", + "resume_paused_jobs": "Nadaljuj {count, plural, one {# zaustavljeno opravilo} two {# zaustavljeni opravili} few {# zaustavljena opravila} other {# zaustavljenih opravil}}", "retry_upload": "Poskusite znova naložiti", "review_duplicates": "Pregled dvojnikov", "review_large_files": "Pregled velikih datotek", @@ -1714,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "Albuma ni bilo mogoče ustvariti", "selected": "Izbrano", "selected_count": "{count, plural, other {# izbranih}}", + "selected_gps_coordinates": "izbrane GPS koordinate", "send_message": "Pošlji sporočilo", "send_welcome_email": "Pošlji pozdravno e-pošto", "server_endpoint": "Končna točka strežnika", @@ -1842,6 +1895,7 @@ "show_slideshow_transition": "Prikaži prehod diaprojekcije", "show_supporter_badge": "Značka podpornika", "show_supporter_badge_description": "Prikaži značko podpornika", + "show_text_search_menu": "Prikaži meni za iskanje po besedilu", "shuffle": "Naključno", "sidebar": "Stranska vrstica", "sidebar_display_description": "Prikaži povezavo do pogleda v stranski vrstici", @@ -1872,6 +1926,7 @@ "stacktrace": "Sled nabora", "start": "Začetek", "start_date": "Datum začetka", + "start_date_before_end_date": "Začetni datum mora biti pred končnim datumom", "state": "Dežela", "status": "Status", "stop_casting": "Ustavi predvajanje", @@ -1896,6 +1951,8 @@ "sync_albums_manual_subtitle": "Sinhronizirajte vse naložene videoposnetke in fotografije v izbrane varnostne albume", "sync_local": "Sinhroniziraj lokalno", "sync_remote": "Sinhroniziraj oddaljeno", + "sync_status": "Stanje sinhronizacije", + "sync_status_subtitle": "Ogled in upravljanje sistema sinhronizacije", "sync_upload_album_setting_subtitle": "Ustvarite in naložite svoje fotografije in videoposnetke v izbrane albume na Immich", "tag": "Oznaka", "tag_assets": "Označi sredstva", @@ -1933,7 +1990,9 @@ "to_change_password": "Spremeni geslo", "to_favorite": "Priljubljen", "to_login": "Prijava", + "to_multi_select": "izbira več elementov", "to_parent": "Pojdi na prvotno", + "to_select": "na izbiro", "to_trash": "Smetnjak", "toggle_settings": "Preklopi na nastavitve", "total": "Skupno", @@ -1953,6 +2012,7 @@ "trash_page_select_assets_btn": "Izberite sredstva", "trash_page_title": "Smetnjak ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementi v smetnjaku bodo trajno izbrisani po {days, plural, one {# dnevu} two {# dnevih} few {# dnevih} other {# dneh}}.", + "troubleshoot": "Odpravljanje težav", "type": "Vrsta", "unable_to_change_pin_code": "PIN kode ni mogoče spremeniti", "unable_to_setup_pin_code": "PIN kode ni mogoče nastaviti", @@ -1983,6 +2043,7 @@ "unstacked_assets_count": "Razloži {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "untagged": "Neoznačeno", "up_next": "Naslednja", + "update_location_action_prompt": "Posodobi lokacijo izbranih sredstev {count} s/z:", "updated_at": "Posodobljeno", "updated_password": "Posodobljeno geslo", "upload": "Naloži", @@ -2049,6 +2110,7 @@ "view_next_asset": "Ogled naslednjega sredstva", "view_previous_asset": "Ogled prejšnjega sredstva", "view_qr_code": "Oglej si kodo QR", + "view_similar_photos": "Oglejte si podobne fotografije", "view_stack": "Ogled sklada", "view_user": "Poglej uporabnika", "viewer_remove_from_stack": "Odstrani iz sklada", @@ -2067,5 +2129,6 @@ "yes": "Da", "you_dont_have_any_shared_links": "Nimate nobenih skupnih povezav", "your_wifi_name": "Vaše ime Wi-Fi", - "zoom_image": "Povečava slike" + "zoom_image": "Povečava slike", + "zoom_to_bounds": "Povečaj do meja" } diff --git a/i18n/sq.json b/i18n/sq.json index 0967ef424b..de7c5faa27 100644 --- a/i18n/sq.json +++ b/i18n/sq.json @@ -1 +1,59 @@ -{} +{ + "about": "Rreth", + "account": "Llogari", + "account_settings": "Cilësimet e Llogarisë", + "acknowledge": "Prano", + "action": "Aksion", + "action_common_update": "Përditëso", + "actions": "Aksione", + "active": "Aktiv", + "activity": "Aktivitet", + "activity_changed": "Aktiviteti është {enabled, select, true {aktivizuar} other {çaktivizuar}}", + "add": "Shto", + "add_a_description": "Shto një përshkrim", + "add_a_location": "Shto një vendndodhje", + "add_a_name": "Shto një emër", + "add_a_title": "Shto një titull", + "add_birthday": "Shto një ditëlindje", + "add_endpoint": "Shto një endpoint", + "add_exclusion_pattern": "Shto model përjashtimi", + "add_import_path": "Shto vënd importimi", + "add_location": "Shto vendndodhje", + "add_more_users": "Shto më shumë përdorues", + "add_partner": "Shto partner", + "add_path": "Shto path", + "add_photos": "Shto foto", + "add_tag": "Shto tag", + "add_to": "Shto në…", + "add_to_album": "Shto në album", + "add_to_album_bottom_sheet_added": "Shtuar në {album}", + "add_to_album_bottom_sheet_already_exists": "Existon në {album}", + "add_to_album_toggle": "Aktivizo/çaktivizo zgjedhjen për {album}", + "add_to_albums": "Shto në albume", + "add_to_albums_count": "Shto në albume ({count})", + "add_to_shared_album": "Shto në album të hapur", + "add_url": "Shto URL", + "added_to_archive": "Shtuar në arkiv", + "added_to_favorites": "Shtuar tek të preferuarat", + "added_to_favorites_count": "Shtuar {count, number} në të preferuarat", + "admin": { + "add_exclusion_pattern_description": "Shto modele përjashtimi. Mbështetet globimi duke përdorur *, ** dhe ?. Për të injoruar të gjithë skedarët në çdo drejtori të quajtur \"Raw\", përdorni \"**/Raw/**\". Për të injoruar të gjithë skedarët që mbarojnë me \".tif\", përdorni \"**/*.tif\". Për të injoruar një shteg absolut, përdorni \"/path/to/ignore/**\".", + "admin_user": "Përdorues Administrator", + "asset_offline_description": "Ky aset i bibliotekës së jashtme nuk gjendet më në disk dhe është zhvendosur në koshin e plehrave. Nëse skedari është zhvendosur brenda bibliotekës, kontrolloni kronologjinë tuaj për asetin e ri përkatës. Për të rivendosur këtë aset, sigurohuni që shtegu i skedarit më poshtë të jetë i arritshëm nga Immich dhe skanoni bibliotekën.", + "authentication_settings": "Cilësimet e vërtetimit të përdoruesit", + "authentication_settings_description": "Manaxho passwordin, OAuth, dhe cilësime të tjera të", + "authentication_settings_disable_all": "Je i sigurt që dëshiron të çaktivizosh të gjitha metodat e hyrjes? Hyrja do të çaktivizohet plotësisht.", + "authentication_settings_reenable": "Për ta riaktivizuar, përdorni një Komandë Serveri.", + "background_task_job": "Detyrat në Sfond", + "backup_database": "Krijo demp të databaseit", + "backup_database_enable_description": "Aktivizo demp-et e bazës së të dhënave", + "backup_keep_last_amount": "Sasia e deponive të mëparshme për t'u mbajtur", + "backup_onboarding_1_description": "kopje në cloud ose në një vendndodhje tjetër fizike.", + "backup_onboarding_2_description": "kopje lokale në pajisje të ndryshme. Kjo përfshin skedarët kryesorë dhe një kopje rezervë të këtyre skedarëve lokalisht.", + "backup_onboarding_3_description": "kopje totale të të dhënave tuaja, duke përfshirë skedarët origjinalë. Kjo përfshin 1 kopje jashtë faqes dhe 2 kopje lokale.", + "backup_onboarding_description": "Rekomandohet një strategji 3-2-1 për ruajtjen e të dhënave tuaja. Duhet të ruani kopje të fotove/videove të ngarkuara, si dhe të bazës së të dhënave të Immich për një zgjidhje gjithëpërfshirëse të ruajtjes së të dhënave.", + "backup_onboarding_footer": "Për më shumë informacion për të krijuar një kopje rezervë të Immich, ju lutem referouni tek dokumentimi.", + "backup_onboarding_parts_title": "Një kopje rezervë 3-2-1 ka:", + "backup_onboarding_title": "Kopje rezervë" + } +} diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index 5b11aa9a98..06a76f8f0a 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -4,6 +4,7 @@ "account_settings": "Podešavanja za Profil", "acknowledge": "Potvrdi", "action": "Postupak", + "action_common_update": "Ažuriraj", "actions": "Postupci", "active": "Aktivni", "activity": "Aktivnost", @@ -13,6 +14,7 @@ "add_a_location": "Dodaj Lokaciju", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", + "add_birthday": "Dodaj rođendan", "add_endpoint": "Dodajte krajnju tačku", "add_exclusion_pattern": "Dodajte obrazac izuzimanja", "add_import_path": "Dodaj putanju za preuzimanje", @@ -21,10 +23,14 @@ "add_partner": "Dodaj partner", "add_path": "Dodaj putanju", "add_photos": "Dodaj fotografije", + "add_tag": "Dodaj oznaku", "add_to": "Dodaj u…", "add_to_album": "Dodaj u album", "add_to_album_bottom_sheet_added": "Dodato u {album}", "add_to_album_bottom_sheet_already_exists": "Već u {album}", + "add_to_album_toggle": "Uključi/isključi izbor za {album}", + "add_to_albums": "Dodaj u albume", + "add_to_albums_count": "Dodaj u albume ({count})", "add_to_shared_album": "Dodaj u deljen album", "add_url": "Dodaj URL", "added_to_archive": "Dodato u arhivu", @@ -32,6 +38,7 @@ "added_to_favorites_count": "Dodato {count, number} u favorite", "admin": { "add_exclusion_pattern_description": "Dodajte obrasce isključenja. Korištenje *, ** i ? je podržano. Da biste ignorisali sve datoteke u bilo kom direktorijumu pod nazivom „Rav“, koristite „**/Rav/**“. Da biste ignorisali sve datoteke koje se završavaju na „.tif“, koristite „**/*.tif“. Da biste ignorisali apsolutnu putanju, koristite „/path/to/ignore/**“.", + "admin_user": "Administrator", "asset_offline_description": "Ovo eksterno bibliotečko sredstvo se više ne nalazi na disku i premešteno je u smeće. Ako je datoteka premeštena unutar biblioteke, proverite svoju vremensku liniju za novo odgovarajuće sredstvo. Da biste vratili ovo sredstvo, uverite se da Immich može da pristupi dole navedenoj putanji datoteke i skenirajte biblioteku.", "authentication_settings": "Podešavanja za autentifikaciju", "authentication_settings_description": "Upravljajte lozinkom, OAuth-om i drugim podešavanjima autentifikacije", @@ -41,8 +48,15 @@ "backup_database": "Kreirajte rezervnu kopiju baze podataka", "backup_database_enable_description": "Omogući dampove baze podataka", "backup_keep_last_amount": "Količina prethodnih dampova koje treba zadržati", + "backup_onboarding_1_description": "kopija na oblaku ili na drugoj fizičkoj lokaciji.", + "backup_onboarding_2_description": "lokalne kopije na različitim uređajima. Ovo uključuje glavne datoteke i rezervnu kopiju tih datoteka lokalno.", + "backup_onboarding_3_description": "ukupno kopija vaših podataka, uklučujući originalne datoteke. Ovo uključuje 1 udaljenu kopiju i 2 lokalne kopije.", + "backup_onboarding_description": "3-2-1 strategija rezervnih kopija je preporučena da zaštiti vaše podatke. Trebali biste čuvati kopije vaših otpremljenih slika/videa kao i Immich bazu podataka za sveobuhvatno rešenje za rezervne kopije.", + "backup_onboarding_footer": "Za više informacija o pravljenju rezervne kopije Immich-a, molimo vas pogledajte dokumentaciju.", + "backup_onboarding_parts_title": "3-2-1 rezervna kopija uključuje:", + "backup_onboarding_title": "Rezervne kopije", "backup_settings": "Podešavanja dampa baze podataka", - "backup_settings_description": "Upravljajte podešavanjima dampa baze podataka. Napomena: Ovi poslovi se ne prate i nećete biti obavešteni o neuspehu.", + "backup_settings_description": "Upravljajte podešavanjima dampa baze podataka.", "cleared_jobs": "Očišćeni poslovi za: {job}", "config_set_by_file": "Konfiguraciju trenutno postavlja konfiguracioni fajl", "confirm_delete_library": "Da li stvarno želite da izbrišete biblioteku {library} ?", @@ -163,12 +177,23 @@ "metadata_settings_description": "Upravljajte podešavanjima metapodataka", "migration_job": "Migracije", "migration_job_description": "Prenesite sličice datoteka i lica u najnoviju strukturu direktorijuma", + "nightly_tasks_cluster_faces_setting_description": "Pokreni prepoznavanje lica na novodetektovanim licima", + "nightly_tasks_cluster_new_faces_setting": "Združi nova lica", + "nightly_tasks_database_cleanup_setting": "Zadaci čiščenja baze podataka", + "nightly_tasks_database_cleanup_setting_description": "Očisti stare, istekle podatke iz baze podataka", + "nightly_tasks_generate_memories_setting": "Generiši sjećanja", + "nightly_tasks_generate_memories_setting_description": "Kreiraj nova sjećanja", + "nightly_tasks_missing_thumbnails_setting": "Generiši nedostajuće sličice", + "nightly_tasks_missing_thumbnails_setting_description": "Dodajte elemente bez sličica u red za generisanje sličica", + "nightly_tasks_settings": "Podešavanja noćnih zadataka", + "nightly_tasks_settings_description": "Upravljaj noćnim zadacima", + "nightly_tasks_sync_quota_usage_setting_description": "Ažurirajte kvotu memorijskog prostora korisnika na osnovu trenutne upotrebe", "no_paths_added": "Nema dodatih putanja", "no_pattern_added": "Nije dodat obrazac", "note_apply_storage_label_previous_assets": "Napomena: Da biste primenili oznaku za skladištenje na prethodno otpremljena sredstva, pokrenite", "note_cannot_be_changed_later": "NAPOMENA: Ovo se kasnije ne može promeniti!", "notification_email_from_address": "Sa adrese", - "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server \"", + "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server \". Pobrinite se da koristite adresu sa koje vam je dozovljeno slati e-poštu.", "notification_email_host_description": "Host servera e-pošte (npr. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Zanemarite greške sertifikata", "notification_email_ignore_certificate_errors_description": "Ignorišite greške u validaciji TLS sertifikata (ne preporučuje se)", @@ -201,7 +226,7 @@ "oauth_storage_quota_claim": "Zahtev za kvotu skladištenja", "oauth_storage_quota_claim_description": "Automatski podesite kvotu memorijskog prostora korisnika na vrednost ovog zahteva.", "oauth_storage_quota_default": "Podrazumevana kvota za skladištenje (GiB)", - "oauth_storage_quota_default_description": "Kvota u GiB koja se koristi kada nema potraživanja (unesite 0 za neograničenu kvotu).", + "oauth_storage_quota_default_description": "Kvota u GiB koja se koristi kada nema potraživanja.", "oauth_timeout": "Vremensko ograničenje zahteva", "oauth_timeout_description": "Vremensko ograničenje za zahteve u milisekundama", "password_enable_description": "Prijavite se pomoću e-pošte i lozinke", @@ -508,7 +533,7 @@ "backup_controller_page_background_turn_off": "Isključi pozadinski servis", "backup_controller_page_background_turn_on": "Uključi pozadinski servis", "backup_controller_page_background_wifi": "Samo na Wi-Fi", - "backup_controller_page_backup": "Napravi rezervnu kopiju", + "backup_controller_page_backup": "Rezervne kopije", "backup_controller_page_backup_selected": "Odabrano: ", "backup_controller_page_backup_sub": "Završeno pravljenje rezervne kopije fotografija i videa", "backup_controller_page_created": "Napravljeno:{date}", @@ -519,8 +544,8 @@ "backup_controller_page_id": "ID:{id}", "backup_controller_page_info": "Informacije", "backup_controller_page_none_selected": "Ništa odabrano", - "backup_controller_page_remainder": "Podsetnik", - "backup_controller_page_remainder_sub": "Ostalo fotografija i videa da se otpremi od selekcije", + "backup_controller_page_remainder": "Ostatak", + "backup_controller_page_remainder_sub": "Ostale fotografije i video snimci za otpremanje od selekcije", "backup_controller_page_server_storage": "Prostor na serveru", "backup_controller_page_start_backup": "Pokreni pravljenje rezervne kopije", "backup_controller_page_status_off": "Automatsko pravljenje rezervnih kopija u prvom planu je isključeno", diff --git a/i18n/sv.json b/i18n/sv.json index 181dff6a02..6acee96f66 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -123,6 +123,13 @@ "logging_enable_description": "Aktivera loggning", "logging_level_description": "Vilken loggnivå som ska användas vid aktivering.", "logging_settings": "Loggning", + "machine_learning_availability_checks": "Tillgänglighetskontroller", + "machine_learning_availability_checks_description": "Upptäck och föredrar automatiskt tillgängliga maskininlärningsservrar", + "machine_learning_availability_checks_enabled": "Aktivera tillgänglighetskontroller", + "machine_learning_availability_checks_interval": "Kontrollera intervall", + "machine_learning_availability_checks_interval_description": "Intervall i millisekunder mellan tillgänglighetskontroller", + "machine_learning_availability_checks_timeout": "Begär timeout", + "machine_learning_availability_checks_timeout_description": "Timeout i millisekunder för tillgänglighetskontroller", "machine_learning_clip_model": "CLIP-modell", "machine_learning_clip_model_description": "Namnet på en CLIP-modell listad här . Observera att du måste köra ett \"Smart Sökning\" jobb för alla bilder när du ändrar modell.", "machine_learning_duplicate_detection": "Dubblettdetektering", @@ -387,8 +394,6 @@ "admin_password": "Admin Lösenord", "administration": "Administration", "advanced": "Avancerat", - "advanced_settings_beta_timeline_subtitle": "Testa den nya appupplevelsen", - "advanced_settings_beta_timeline_title": "Tidslinje(BETA)", "advanced_settings_enable_alternate_media_filter_subtitle": "Använd det här alternativet för att filtrera media under synkronisering baserat på alternativa kriterier. Prova detta endast om du har problem med att appen inte hittar alla album.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTELLT] Använd alternativ enhetsalbum-synkroniseringsfilter", "advanced_settings_log_level_title": "Loggnivå: {level}", @@ -396,6 +401,8 @@ "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", "advanced_settings_proxy_headers_title": "Proxy-headers", + "advanced_settings_readonly_mode_subtitle": "Aktiverar skrivskyddat läge där foton endast kan visas. Saker som att välja flera bilder, dela, casta och ta bort är alla inaktiverade. Aktivera/inaktivera skrivskyddat läge via användaravatar från huvudskärmen", + "advanced_settings_readonly_mode_title": "Skrivskyddat läge", "advanced_settings_self_signed_ssl_subtitle": "Hoppar över SSL-certifikatverifiering för serverändpunkten. Krävs för självsignerade certifikat.", "advanced_settings_self_signed_ssl_title": "Tillåt självsignerade SSL-certifikat", "advanced_settings_sync_remote_deletions_subtitle": "Radera eller återställ automatiskt en resurs på den här enheten när den åtgärden utförs på webben", @@ -423,6 +430,7 @@ "album_remove_user_confirmation": "Är du säker på att du vill ta bort {user}?", "album_search_not_found": "Inga album hittades som matchade din sökning", "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_summary": "Albumsammanfattning", "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}", @@ -461,6 +469,7 @@ "app_bar_signout_dialog_title": "Logga ut", "app_settings": "Appinställningar", "appears_in": "Visas i", + "apply_count": "Tillämpa ({count, number})", "archive": "Arkiv", "archive_action_prompt": "{count} adderade till Arkiv", "archive_or_unarchive_photo": "Arkivera eller oarkivera fotot", @@ -493,6 +502,8 @@ "asset_restored_successfully": "Objekt återställt", "asset_skipped": "Överhoppad", "asset_skipped_in_trash": "I papperskorgen", + "asset_trashed": "Tillgång kasserad", + "asset_troubleshoot": "Felsökning av tillgångar", "asset_uploaded": "Uppladdad", "asset_uploading": "Laddar upp...…", "asset_viewer_settings_subtitle": "Hantera inställningar för gallerivisare", @@ -500,7 +511,7 @@ "assets": "Objekt", "assets_added_count": "La till {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Lade till {count, plural, one {# asset} other {# assets}} i albumet", - "assets_added_to_albums_count": "Lade till {assetTotal, plural, one {# asset} other {# assets}} till {albumTotal} album", + "assets_added_to_albums_count": "Lade till {assetTotal, plural, one {# asset} other {# assets}} till {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} kan inte läggas till i albumet", "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} kan inte läggas till i något av albumen", "assets_count": "{count, plural, one {# objekt} other {# objekt}}", @@ -526,8 +537,10 @@ "autoplay_slideshow": "Spela upp bildspel automatiskt", "back": "Bakåt", "back_close_deselect": "Tillbaka, stäng eller avmarkera", + "background_backup_running_error": "Bakgrundssäkerhetskopiering körs för närvarande, kan inte starta manuell säkerhetskopiering", "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", + "background_options": "Bakgrundsalternativ", "backup": "Säkerhetskopiera", "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", @@ -535,6 +548,7 @@ "backup_album_selection_page_select_albums": "Välj album", "backup_album_selection_page_selection_info": "Info om valda objekt", "backup_album_selection_page_total_assets": "Antal unika objekt", + "backup_albums_sync": "Säkerhetskopiera album synkronisering", "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…", @@ -594,8 +608,6 @@ "backup_setting_subtitle": "Hantera inställningar för för- och bakgrundsuppladdning", "backup_settings_subtitle": "Hantera uppladdningsinställningar", "backward": "Bakåt", - "beta_sync": "Synkroniseringsstatus(BETA)", - "beta_sync_subtitle": "Hantera det nya synkroniseringssystemet", "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", @@ -653,6 +665,8 @@ "change_pin_code": "Ändra PIN-kod", "change_your_password": "Ändra ditt lösenord", "changed_visibility_successfully": "Synligheten har ändrats", + "charging": "Laddar", + "charging_requirement_mobile_backup": "Bakgrundssäkerhetskopiering kräver att enheten laddas", "check_corrupt_asset_backup": "Kontrollera om det finns korrupta säkerhetskopior av objekt", "check_corrupt_asset_backup_button": "Kontrollera", "check_corrupt_asset_backup_description": "Kör kontrollen endast över Wi-Fi och när alla objekt har säkerhetskopierats. Det kan ta några minuter.", @@ -739,6 +753,7 @@ "create_user": "Skapa användare", "created": "Skapad", "created_at": "Skapad", + "creating_linked_albums": "Skapar länkade album...", "crop": "Beskär", "curated_object_page_title": "Objekt", "current_device": "Aktuell enhet", @@ -870,7 +885,7 @@ "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", + "editor_crop_tool_h2_rotation": "Vridning", "email": "Epost", "email_notifications": "E-postaviseringar", "empty_folder": "Mappen är tom", @@ -888,7 +903,9 @@ "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_getting_places": "Det gick inte att hämta platser", "error_loading_image": "Fel vid bildladdning", + "error_loading_partners": "Fel vid inläsning av partner: {error}", "error_saving_image": "Fel: {error}", "error_tag_face_bounding_box": "Fel vid taggning av ansikte – kan inte hämta koordinater för begränsningsruta", "error_title": "Fel – något gick fel", @@ -1014,7 +1031,7 @@ "unable_to_update_user": "Kunde inte uppdatera användare", "unable_to_upload_file": "Det går inte att ladda upp filen" }, - "exif": "Exif", + "exif": "EXIF", "exif_bottom_sheet_description": "Lägg till beskrivning...", "exif_bottom_sheet_description_error": "Fel vid uppdatering av beskrivningen", "exif_bottom_sheet_details": "DETALJER", @@ -1053,6 +1070,7 @@ "favorites_page_no_favorites": "Inga favoritobjekt hittades", "feature_photo_updated": "Funktionsfoto uppdaterad", "features": "Funktioner", + "features_in_development": "Funktioner i utveckling", "features_setting_description": "Hantera appens funktioner", "file_name": "Filnamn", "file_name_or_extension": "Filnamn eller -tillägg", @@ -1070,15 +1088,18 @@ "folders_feature_description": "Bläddra i mappvyn för foton och videoklipp i filsystemet", "forgot_pin_code_question": "Glömt din pinkod?", "forward": "Framåt", - "gcast_enabled": "Google Cast", + "gcast_enabled": "Google-Cast", "gcast_enabled_description": "Denna funktion läser in externa resurser från Google för att fungera.", "general": "Allmänt", + "geolocation_instruction_location": "Klicka på en tillgång med GPS-koordinater för att använda dess plats, eller välj en plats direkt från kartan", "get_help": "Få hjälp", "get_wifiname_error": "Kunde inte hämta Wi-Fi-namn. Säkerställ att du tillåtit nödvändiga rättigheter och är ansluten till ett Wi-Fi-nätverk", "getting_started": "Komma igång", "go_back": "Gå tillbaka", "go_to_folder": "Gå till mapp", "go_to_search": "Gå till sök", + "gps": "GPS", + "gps_missing": "Ingen GPS", "grant_permission": "Ge tillåtelse", "group_albums_by": "Gruppera album efter...", "group_country": "Gruppera per land", @@ -1209,11 +1230,12 @@ "link_to_oauth": "Länk till OAuth", "linked_oauth_account": "Länkat OAuth konto", "list": "Lista", - "loading": "Laddar", + "loading": "Inläsning", "loading_search_results_failed": "Det gick inte att läsa in sökresultat", "local": "Lokalt", "local_asset_cast_failed": "Det går inte att casta en tillgång som inte har laddats upp till servern", "local_assets": "Lokala tillgångar", + "local_media_summary": "Sammanfattning av lokala medier", "local_network": "Lokalt nätverk", "local_network_sheet_info": "Appen kommer ansluta till servern via denna URL när det specificerade WiFi-nätverket används", "location_permission": "Plats-rättighet", @@ -1225,6 +1247,7 @@ "location_picker_longitude_hint": "Ange din longitud här", "lock": "Lås", "locked_folder": "Låst Mapp", + "log_detail_title": "Loggdetalj", "log_out": "Logga ut", "log_out_all_devices": "Logga ut alla enheter", "logged_in_as": "Inloggad som {user}", @@ -1255,6 +1278,7 @@ "login_password_changed_success": "Uppdatering av lösenord lyckades", "logout_all_device_confirmation": "Är du säker på att du vill logga ut från alla enheter?", "logout_this_device_confirmation": "Är du säker på att du vill logga ut från denna enhet?", + "logs": "Loggar", "longitude": "Longitud", "look": "Titta", "loop_videos": "Loopa videor", @@ -1262,6 +1286,7 @@ "main_branch_warning": "Du använder en utvecklingsversion. Vi rekommenderar starkt att du använder en utgiven version!", "main_menu": "Huvudmeny", "make": "Tillverkare", + "manage_geolocation": "Hantera plats", "manage_shared_links": "Hantera Delade länkar", "manage_sharing_with_partners": "Hantera delning med partner", "manage_the_app_settings": "Hantera appinställningarna", @@ -1296,6 +1321,7 @@ "mark_as_read": "Markera som läst", "marked_all_as_read": "Markerade alla som lästa", "matches": "Matchar", + "matching_assets": "Matchande tillgångar", "media_type": "Mediatyp", "memories": "Minnen", "memories_all_caught_up": "Du är ikapp", @@ -1336,6 +1362,7 @@ "name_or_nickname": "Namn eller smeknamn", "network_requirement_photos_upload": "Använd mobildata för att säkerhetskopiera foton", "network_requirement_videos_upload": "Använd mobildata för att säkerhetskopiera videor", + "network_requirements": "Nätverkskrav", "network_requirements_updated": "Nätverkskraven har ändrats, återställer säkerhetskopieringskön", "networking_settings": "Nätverk", "networking_subtitle": "Hantera inställningar för server-endpointen", @@ -1346,6 +1373,7 @@ "new_person": "Ny person", "new_pin_code": "Ny PIN-kod", "new_pin_code_subtitle": "Det här är första gången du öppnar den låsta mappen. Skapa en PIN-kod för att säkert få åtkomst till den här sidan", + "new_timeline": "Ny tidslinje", "new_user_created": "Ny användare skapad", "new_version_available": "NY VERSION TILLGÄNGLIG", "newest_first": "Nyast först", @@ -1359,20 +1387,25 @@ "no_assets_message": "KLICKA FÖR ATT LADDA UPP DIN FÖRSTA BILD", "no_assets_to_show": "Inga objekt att visa", "no_cast_devices_found": "Inga Cast-enheter hittades", + "no_checksum_local": "Ingen kontrollsumma tillgänglig - kan inte hämta lokala tillgångar", + "no_checksum_remote": "Ingen kontrollsumma tillgänglig - kan inte hämta fjärrtillgång", "no_duplicates_found": "Inga dubbletter hittades.", "no_exif_info_available": "EXIF-information ej tillgänglig", "no_explore_results_message": "Ladda upp fler bilder för att utforska din samling.", "no_favorites_message": "Lägg till favoriter för att snabbt hitta dina bästa bilder och videor", "no_libraries_message": "Skapa ett externt bibliotek för att se dina bilder och videor", + "no_local_assets_found": "Inga lokala tillgångar hittades med denna kontrollsumma", "no_locked_photos_message": "Foton och videor i den låsta mappen är dolda och visas inte när du bläddrar eller söker i ditt bibliotek.", "no_name": "Inget namn", "no_notifications": "Inga aviseringar", "no_people_found": "Inga matchande personer hittade", "no_places": "Inga platser", + "no_remote_assets_found": "Inga fjärrtillgångar hittades med denna kontrollsumma", "no_results": "Inga resultat", "no_results_description": "Pröva en synonym eller ett annat mer allmänt sökord", "no_shared_albums_message": "Skapa ett album för att dela bilder och videor med andra personer", "no_uploads_in_progress": "Inga uppladdningar pågår", + "not_available": "N/A", "not_in_any_album": "Inte i något album", "not_selected": "Ej vald", "note_apply_storage_label_to_previously_uploaded assets": "Obs: Om du vill använda lagringsetiketten på tidigare uppladdade tillgångar kör du", @@ -1407,6 +1440,8 @@ "open_the_search_filters": "Öppna sökfilter", "options": "Val", "or": "eller", + "organize_into_albums": "Organisera i album", + "organize_into_albums_description": "Lägg befintliga foton i album med aktuella synkroniseringsinställningar", "organize_your_library": "Organisera ditt bibliotek", "original": "original", "other": "Övrigt", @@ -1492,6 +1527,7 @@ "port": "Port", "preferences_settings_subtitle": "Hantera appens inställningar", "preferences_settings_title": "Inställningar", + "preparing": "Förbereder", "preset": "Förinställt värde", "preview": "Förhandsvisning", "previous": "Föregående", @@ -1504,17 +1540,18 @@ "privacy": "Sekretess", "profile": "Profil", "profile_drawer_app_logs": "Loggar", - "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_out_of_date_major": "Mobilappen är föråldrad. Uppdatera till senaste versionen.", + "profile_drawer_client_out_of_date_minor": "Mobilappen är föråldrad. Uppdatera till senaste 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_drawer_readonly_mode": "Skrivskyddat läge aktiverat. Långtryck användaravatarikonen för att avsluta.", + "profile_drawer_server_out_of_date_major": "Servern har en föråldrad mjukvara. Uppdatera till senaste versionen.", + "profile_drawer_server_out_of_date_minor": "Servern har en föråldrad mjukvara. Uppdatera till senaste 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_account_info": "Anhängare", "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", @@ -1546,6 +1583,7 @@ "purchase_server_description_2": "Supporterstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktnyckeln för servern hanteras av administratören", + "query_asset_id": "Fråga om objekts-ID", "queue_status": "Köande {count}/{total}", "rating": "Antal stjärnor", "rating_clear": "Ta bort betyg", @@ -1553,6 +1591,9 @@ "rating_description": "Visa EXIF betyget i informationspanelen", "reaction_options": "Alternativ för reaktion", "read_changelog": "Läs ändringslogg", + "readonly_mode_disabled": "Skrivskyddat läge inaktiverat", + "readonly_mode_enabled": "Skrivskyddat läge aktiverat", + "ready_for_upload": "Redo för uppladdning", "reassign": "Omfördela", "reassigned_assets_to_existing_person": "Tilldelade om {count, plural, one {# objekt} other {# objekt}} till {name, select, null {an existing person} other {{name}}}", "reassigned_assets_to_new_person": "Tilldelade om {count, plural, one {# objekt} other {# objekt}} till en ny persson", @@ -1577,6 +1618,7 @@ "regenerating_thumbnails": "Uppdaterar miniatyrer", "remote": "Fjärrr", "remote_assets": "Fjärrtillgångar", + "remote_media_summary": "Sammanfattning av fjärrmedia", "remove": "Ta bort", "remove_assets_album_confirmation": "Är du säker på att du vill ta bort {count, plural, one {# asset} other {# assets}} från albumet?", "remove_assets_shared_link_confirmation": "Är du säker på att du vill ta bort {count, plural, one {# asset} other {# assets}} från denna delade länk?", @@ -1629,6 +1671,7 @@ "restore_user": "Återställ användare", "restored_asset": "Återställ tillgång", "resume": "Återuppta", + "resume_paused_jobs": "Återuppta {count, plural, one {# pausat jobb} other {# pausade jobb}}", "retry_upload": "Ladda upp igen", "review_duplicates": "Granska dubbletter", "review_large_files": "Granska stora filer", @@ -1722,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album", "selected": "Valda", "selected_count": "{count, plural, other {# valda}}", + "selected_gps_coordinates": "Valda GPS-koordinater", "send_message": "Skicka meddelande", "send_welcome_email": "Skicka välkomstmejl", "server_endpoint": "Server-endpoint", @@ -1850,6 +1894,7 @@ "show_slideshow_transition": "Visa bildspelsövergång", "show_supporter_badge": "Supporteremblem", "show_supporter_badge_description": "Visa supporteremblem", + "show_text_search_menu": "Visa textsökningsmeny", "shuffle": "Blanda", "sidebar": "Sidopanel", "sidebar_display_description": "Visa en länk till vyn i sidofältet", @@ -1880,6 +1925,7 @@ "stacktrace": "Stapelspårning", "start": "Starta", "start_date": "Startdatum", + "start_date_before_end_date": "Startdatumet måste vara före slutdatumet", "state": "Stat", "status": "Status", "stop_casting": "Sluta casta", @@ -1896,7 +1942,7 @@ "suggestions": "Förslag", "sunrise_on_the_beach": "Soluppgång på stranden", "support": "Support", - "support_and_feedback": "Support & Feedback", + "support_and_feedback": "Support och 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", @@ -1904,6 +1950,8 @@ "sync_albums_manual_subtitle": "Synka alla uppladdade videor och foton till valda backup-album", "sync_local": "Synkronisera lokalt", "sync_remote": "Synkronisera fjärrserver", + "sync_status": "Synk Status", + "sync_status_subtitle": "Visa och hantera synkroniseringssystemet", "sync_upload_album_setting_subtitle": "Skapa och ladda upp dina foton och videor till de valda albumen på Immich", "tag": "Tagg", "tag_assets": "Tagga tillgångar", @@ -1941,7 +1989,9 @@ "to_change_password": "Ändra lösenord", "to_favorite": "Favorit", "to_login": "Logga in", + "to_multi_select": "för att välja flera", "to_parent": "Gå till förälder", + "to_select": "för att välja", "to_trash": "Papperskorg", "toggle_settings": "Växla inställningar", "total": "Totalt", @@ -1961,6 +2011,7 @@ "trash_page_select_assets_btn": "Välj objekt", "trash_page_title": "Papperskorg ({count})", "trashed_items_will_be_permanently_deleted_after": "Objekt i papperskorgen raderas permanent efter {days, plural, one {# dag} other {# dagar}}.", + "troubleshoot": "Felsök", "type": "Typ", "unable_to_change_pin_code": "Kunde inte ändra pinkod", "unable_to_setup_pin_code": "Kunde inte konfigurera pinkod", @@ -1991,6 +2042,7 @@ "unstacked_assets_count": "Avstaplade {count, plural, one {# asset} other {# assets}}", "untagged": "Otaggad", "up_next": "Kommande", + "update_location_action_prompt": "Uppdatera platsen för {count} valda tillgångar med:", "updated_at": "Uppdaterat", "updated_password": "Lösenordet har uppdaterats", "upload": "Ladda upp", @@ -2057,6 +2109,7 @@ "view_next_asset": "Visa nästa objekt", "view_previous_asset": "Visa föregående objekt", "view_qr_code": "Visa QR-kod", + "view_similar_photos": "Visa liknande foton", "view_stack": "Visa Stapel", "view_user": "Visa Användare", "viewer_remove_from_stack": "Ta bort från Stapeln", @@ -2075,5 +2128,6 @@ "yes": "Ja", "you_dont_have_any_shared_links": "Du har inga delade länkar", "your_wifi_name": "Ditt Wi-Fi-namn", - "zoom_image": "Zooma bild" + "zoom_image": "Zooma bild", + "zoom_to_bounds": "Zooma till gränser" } diff --git a/i18n/ta.json b/i18n/ta.json index 683c071d0b..f8996ad44b 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -4,16 +4,18 @@ "account_settings": "கணக்கு அமைவுகள்", "acknowledge": "ஒப்புக்கொள்கிறேன்", "action": "செயல்", + "action_common_update": "மேம்படுத்து", "actions": "செயல்கள்", "active": "செயல்பாட்டில்", "activity": "செயல்பாடுகள்", - "activity_changed": "செயல்பாடு {இயக்கப்பட்டது, தேர்ந்தெடு, சரி {enabled} மற்றது {disabled}}", + "activity_changed": "செயல்பாடு {enabled, select, true {இயக்கப்பட்டது} other {முடக்கப்பட்டது}}", "add": "சேர்", "add_a_description": "விவரம் சேர்", "add_a_location": "இடத்தை சேர்க்கவும்", "add_a_name": "பெயரை சேர்க்கவும்", "add_a_title": "தலைப்பு சேர்க்கவும்", "add_birthday": "பிறந்தநாளைச் சேர்க்கவும்", + "add_endpoint": "சேவை நிரலை சேர்", "add_exclusion_pattern": "விலக்கு வடிவத்தைச் சேர்க்கவும்", "add_import_path": "இறக்குமதி பாதையை (இம்போர்ட் பாத்) சேர்க்கவும்", "add_location": "இடத்தைச் சேர்க்கவும்", @@ -22,15 +24,18 @@ "add_path": "பாதை (பாத்) சேர்க்கவும்", "add_photos": "புகைப்படங்களை சேர்க்கவும்", "add_tag": "குறியைச் சேர்க்க", - "add_to": "சேர்க்க...", + "add_to": "சேர்க்கவும்…", "add_to_album": "ஆல்பமில் சேர்க்க", "add_to_album_bottom_sheet_added": "{album}-இல் சேர்க்கப்பட்டது", "add_to_album_bottom_sheet_already_exists": "ஏற்கனவே {album}-இல் உள்ளது", + "add_to_album_toggle": "{album} க்கான தேர்வை மாற்று", + "add_to_albums": "ஆல்பத்தில் சேர்", + "add_to_albums_count": "ஆல்பங்களில் சேர்({count})", "add_to_shared_album": "பகிரப்பட்ட ஆல்பமில் சேர்க்க", "add_url": "URL ஐச் சேர்க்கவும்", "added_to_archive": "காப்பகத்தில் சேர்க்கப்பட்டது", "added_to_favorites": "விருப்பங்களில் (பேவரிட்ஸ்) சேர்க்கப்பட்டது", - "added_to_favorites_count": "விருப்பங்களில் (பேவரிட்ஸ்) {count} சேர்க்கப்பட்டது", + "added_to_favorites_count": "விருப்பங்களில் {count, number} சேர்க்கப்பட்டது", "admin": { "add_exclusion_pattern_description": "விலக்கு வடிவங்களைச் சேர்க்கவும். *, **, மற்றும் ? ஆதரிக்கப்படுகிறது. \"Raw\" என்ற பெயரிடப்பட்ட எந்த கோப்பகத்திலும் உள்ள எல்லா கோப்புகளையும் புறக்கணிக்க, \"**/Raw/**\" ஐப் பயன்படுத்தவும். \".tif\" இல் முடியும் எல்லா கோப்புகளையும் புறக்கணிக்க, \"**/*.tif\" ஐப் பயன்படுத்தவும். ஒரு முழுமையான பாதையை புறக்கணிக்க, \"/path/to/ignore/**\" ஐப் பயன்படுத்தவும்.", "admin_user": "நிர்வாக பயனர்", @@ -40,19 +45,26 @@ "authentication_settings_disable_all": "எல்லா உள்நுழைவு முறைகளையும் நிச்சயமாக முடக்க விரும்புகிறீர்களா? உள்நுழைவு முற்றிலும் முடக்கப்படும்.", "authentication_settings_reenable": "மீண்டும் இயக்க, சர்வர் கட்டளை பயன்படுத்தவும்.", "background_task_job": "பின்னணி பணிகள்", - "backup_database": "காப்பு தரவுத்தளம்", - "backup_database_enable_description": "தரவுத்தள காப்புப்பிரதிகளை இயக்கவும்", - "backup_keep_last_amount": "வைத்திருக்க முந்தைய காப்புப்பிரதிகளின் அளவு", - "backup_onboarding_1_description": "கிளவுட் அல்லது வேறு இடத்தில் ஆஃப்சைட் நகல்.", - "backup_settings": "காப்பு அமைப்புகள்", - "backup_settings_description": "தரவுத்தள காப்புப்பிரதி அமைப்புகளை நிர்வகிக்கவும்", + "backup_database": "தரவுத்தள காப்புப்பிரதியை உருவாக்கு", + "backup_database_enable_description": "தரவுத்தள திணிப்புகள் இயக்கவும்", + "backup_keep_last_amount": "வைத்திருக்க முந்தைய திணிப்புகள் அளவு", + "backup_onboarding_1_description": "மேகக்கட்டத்தில் அல்லது மற்றொரு உடல் இடத்தில் ஆஃப்சைட் நகல்.", + "backup_onboarding_2_description": "வெவ்வேறு சாதனங்களில் உள்ள நகல் பிரதிகள். இதில் முக்கிய கோப்புகள் மற்றும் அந்தக் கோப்புகளின் நகல் காப்புப்பிரதி ஆகியவை அடங்கும்.", + "backup_onboarding_3_description": "உங்கள் தரவின் மொத்த கோப்புகள் அசல் மற்றும் நகல்கள் உட்பட. இதில் 1 வெளிப்புற நகல் மற்றும் 2 சாதனப் பிரதிகள் அடங்கும்.", + "backup_onboarding_description": "உங்கள் தரவை பாதுகாப்பதற்காக ஒரு 3-2-1 காப்புப் பிரதி பரிந்துரைக்கப்படுகிறது. முழுமையான காப்பு பாதுகாப்பு தீர்விற்காக, நீங்கள் பதிவேற்றிய புகைப்படங்கள்/வீடியோக்கள் மற்றும் Immich தரவுத்தளத்தின் நகல்களையும் வைத்திருக்க வேண்டும்.", + "backup_onboarding_footer": "Immich-ஐ தரவு நகல் காப்பு எடுப்பது பற்றிய மேலும் தகவலுக்கு, தயவுசெய்து ஆவணத்தை பார்க்கவும்.", + "backup_onboarding_parts_title": "3-2-1 காப்புப்பிரதியில் பின்வருவன அடங்கும்:", + "backup_onboarding_title": "காப்புப்பிரதிகள்", + "backup_settings": "தரவுத்தள திணிப்பு அமைப்புகள்", + "backup_settings_description": "தரவுத்தள திணிப்பு அமைப்புகளை நிர்வகிக்கவும்", "cleared_jobs": "முடித்த வேலைகள்: {job}", - "config_set_by_file": "config தற்போது ஒரு config கோப்பு மூலம் அமைக்கப்பட்டுள்ளது", + "config_set_by_file": "கட்டமைப்பு, தற்போது ஒரு கட்டமைப்பு கோப்பு மூலம் அமைக்கப்பட்டுள்ளது", "confirm_delete_library": "{library} படங்கள் நூலகத்தை நிச்சயமாக நீக்க விரும்புகிறீர்களா?", "confirm_delete_library_assets": "இந்த நூலகத்தை நிச்சயமாக நீக்க விரும்புகிறீர்களா? இது Immich இலிருந்து {count, plural, one {# contained asset} other {all # contained assets}} நீக்கிவிடும், மேலும் செயல்தவிர்க்க முடியாது. கோப்புகள் வட்டில் இருக்கும்.", "confirm_email_below": "உறுதிப்படுத்த, கீழே \"{email}\" என தட்டச்சு செய்யவும்", "confirm_reprocess_all_faces": "எல்லா முகங்களையும் மீண்டும் செயலாக்க விரும்புகிறீர்களா? இது பெயரிடப்பட்ட நபர்களையும் அழிக்கும்.", "confirm_user_password_reset": "{user} இன் கடவுச்சொல்லை நிச்சயமாக மீட்டமைக்க விரும்புகிறீர்களா?", + "confirm_user_pin_code_reset": "{user} இன் பின் குறியீட்டை மீட்டமைக்க விரும்புகிறீர்களா?", "create_job": "வேலையை உருவாக்கு", "cron_expression": "க்ரோன் வெளிப்பாடு", "cron_expression_description": "CRON வடிவமைப்பைப் பயன்படுத்தி ச்கேனிங் இடைவெளியை அமைக்கவும். மேலும் தகவலுக்கு எ.கா. <இணைப்பு> க்ரோன்டாப் குரு ", @@ -68,8 +80,13 @@ "force_delete_user_warning": "எச்சரிக்கை: இது பயனரையும் அனைத்து புகைப்பட சொத்துகளையும் உடனடியாக அகற்றும். இதை செயல்தவிர்க்க முடியாது மற்றும் புகைப்படங்களை மீட்டெடுக்க முடியாது.", "image_format": "வடிவம்", "image_format_description": "WebP, JPEG ஐ விட சிறிய கோப்புகளை உருவாக்குகிறது, ஆனால் குறியாக்கம் செய்ய மெதுவாக உள்ளது.", + "image_fullsize_description": "பெரிதாக்கும்போது பயன்படுத்தப்படும், அகற்றப்பட்ட மெட்டாடேட்டாவுடன் கூடிய முழு அளவிலான படம்", + "image_fullsize_enabled": "முழு அளவிலான பட உருவாக்கத்தை இயக்கு", + "image_fullsize_enabled_description": "இணைய இணக்கமில்லா வடிவங்களுக்கு முழு அளவிலான படத்தை உருவாக்கவும். \"உட்பொதிக்கப்பட்ட மாதிரிக்காட்சியை விரும்பு\" இயக்கப்பட்டிருக்கும் போது, உட்பொதிக்கப்பட்ட மாதிரிக்காட்சிகள் மாற்றமின்றி நேரடியாகப் பயன்படுத்தப்படும். JPEG போன்ற இணைய நட்பு வடிவங்களைப் பாதிக்காது.", + "image_fullsize_quality_description": "முழு அளவிலான படத் தரம் 1-100 வரை. அதிகமான எண் சிறந்தது, ஆனால் பெரிய கோப்புகளை உருவாக்கும்.", + "image_fullsize_title": "முழு அளவிலான பட அமைப்புகள்", "image_prefer_embedded_preview": "உட்பொதிந்த படத்தை முன்னிடு", - "image_prefer_embedded_preview_setting_description": "கிடைக்கும்போது பட செயலாக்கத்திற்கான உள்ளீடாக மூல புகைப்படங்களில் உட்பொதிக்கப்பட்ட மாதிரிக்காட்சிகளைப் பயன்படுத்தவும். இது சில படங்களுக்கு மிகவும் துல்லியமான வண்ணங்களை உருவாக்க முடியும், ஆனால் முன்னோட்டத்தின் தகுதி கேமரா சார்ந்தது மற்றும் படத்தில் அதிக சுருக்க கலைப்பொருட்கள் இருக்கலாம்.", + "image_prefer_embedded_preview_setting_description": "பட செயலாக்கத்திற்கான உள்ளீடாக மூல புகைப்படங்களில் உட்பொதிக்கப்பட்ட முன்னோட்டங்களை பயன்படுத்தவும், கிடைக்கும்போது. இது சில படங்களுக்கு மிகவும் துல்லியமான வண்ணங்களை உருவாக்க முடியும், ஆனால் முன்னோட்டத்தின் தகுதி கேமரா சார்ந்தது மற்றும் படத்தில் அதிக சுருக்க கலைப்பொருட்கள் இருக்கலாம்.", "image_prefer_wide_gamut": "அகன்ற வண்ணவரம்பு தேர்வு", "image_prefer_wide_gamut_setting_description": "சிறு உருவங்களுக்கு காட்சி பி 3 ஐப் பயன்படுத்தவும். இது பரந்த வண்ணங்களைக் கொண்ட படங்களின் அதிர்வுகளை சிறப்பாக பாதுகாக்கிறது, ஆனால் பழைய உலாவி பதிப்பைக் கொண்ட பழைய சாதனங்களில் படங்கள் வித்தியாசமாக தோன்றக்கூடும். வண்ண மாற்றங்களைத் தவிர்க்க SRGB படங்கள் SRGB ஆக வைக்கப்படுகின்றன.", "image_preview_description": "அகற்றப்பட்ட மெட்டாடேட்டாவுடன் நடுத்தர அளவிலான படம், ஒற்றை சொத்தைப் பார்க்கும்போது மற்றும் இயந்திர கற்றலுக்காகப் பயன்படுத்தப்படுகிறது", @@ -89,8 +106,8 @@ "job_settings": "வேலை அமைப்புகள்", "job_settings_description": "வேலை ஒத்திசைவை நிர்வகிக்கவும்", "job_status": "வேலை நிலை", - "jobs_delayed": "{JobCount, பன்மை, பிற {# தாமதமானது}}", - "jobs_failed": "{JobCount, பன்மை, பிற {# தோல்வியுற்றது}}", + "jobs_delayed": "{jobCount, plural, other {# தாமதமானது}}", + "jobs_failed": "{jobCount, plural, other {# தோல்வியுற்றது}}", "library_created": "உருவாக்கப்பட்ட புகைப்பட நூலகம்: {library}", "library_deleted": "புகைப்பட நூலகம் நீக்கப்பட்டது", "library_import_path_description": "இறக்குமதி செய்ய ஒரு கோப்புறையைக் குறிப்பிடவும். துணைக் கோப்புறைகள் உட்பட இந்தக் கோப்புறை படங்கள் மற்றும் வீடியோக்களுக்காக ஸ்கேன் செய்யப்படும்.", @@ -99,13 +116,20 @@ "library_scanning_enable_description": "நியமிக்கப்பட்ட புகைப்பட நூலக ஸ்கேனிங்கை இயக்கு", "library_settings": "வெளிப்புற புகைப்பட நூலகம்", "library_settings_description": "வெளிப்புற புகைப்பட நூலக அமைப்புகளை மேலாண்மை செய்யவும்", - "library_tasks_description": "நூலக பணிகளைச் செய்யுங்கள்", + "library_tasks_description": "புதிய மற்றும்/அல்லது மாற்றப்பட்ட சொத்துக்களுக்கு வெளிப்புற நூலகங்களை ஸ்கேன் செய்யவும்.", "library_watching_enable_description": "கோப்பு மாற்றங்களுக்கு வெளிப்புற நூலகங்களைப் பாருங்கள்", "library_watching_settings": "நூலகப் பார்ப்பது (சோதனை)", "library_watching_settings_description": "மாற்றப்பட்ட புகைப்படங்களைத் தானாகவே பார்க்கவும்", "logging_enable_description": "பதிவு செய்வதை இயக்கு", "logging_level_description": "இயக்கப்பட்டால், எந்தப் பதிவு நிலை பயன்படுத்த வேண்டும்.", "logging_settings": "பதிவு செய்தல்", + "machine_learning_availability_checks": "கிடைக்கும் காசோலைகள்", + "machine_learning_availability_checks_description": "கிடைக்கக்கூடிய இயந்திர கற்றல் சேவையகங்களை தானாக கண்டறிந்து விரும்புங்கள்", + "machine_learning_availability_checks_enabled": "கிடைக்கும் காசோலைகளை இயக்கவும்", + "machine_learning_availability_checks_interval": "இடைவெளியை சரிபார்க்கவும்", + "machine_learning_availability_checks_interval_description": "கிடைக்கும் காசோலைகளுக்கு இடையில் மில்லி விநாடிகளில் இடைவெளி", + "machine_learning_availability_checks_timeout": "நேரம் முடிந்தது", + "machine_learning_availability_checks_timeout_description": "கிடைக்கும் காசோலைகளுக்கு மில்லி விநாடிகளில் நேரம் முடிந்தது", "machine_learning_clip_model": "கிளிப் மாடல்", "machine_learning_clip_model_description": "CLIP மாடல் பெயர் இங்கே பட்டியலிடப்பட்டுள்ளது. ஒரு மாடயலை மாற்றியவுடன் அனைத்து படங்களுக்கும் 'ஸ்மார்ட் தேடல்' வேலையை மீண்டும் இயக்க வேண்டும் என்பதை நினைவில் கொள்ளவும்.", "machine_learning_duplicate_detection": "நகல் (டூப்ளிகேட்) கண்டறிதல்", @@ -134,7 +158,7 @@ "machine_learning_smart_search_description": "CLIP மாடெலைப் பயன்படுத்தி சொற்பொருளில் படங்களைத் தேடுங்கள்", "machine_learning_smart_search_enabled": "ஸ்மார்ட் தேடலை இயக்கு", "machine_learning_smart_search_enabled_description": "முடக்கப்பட்டிருந்தால், ஸ்மார்ட் தேடலுக்காக படங்கள் குறியாக்கம் செய்யப்படாது.", - "machine_learning_url_description": "இயந்திர கற்றல் சேவையகத்தின் முகவரி. ஒன்றுக்கு மேற்பட்ட முகவரி வழங்கப்பட்டால், ஒவ்வொரு சேவையகமும் வெற்றிகரமாக பதிலளிக்கும் வரை, முதல் முதல் கடைசி வரை முயற்சிக்கும்.", + "machine_learning_url_description": "இயந்திர கற்றல் சேவையகத்தின் முகவரி. ஒன்றுக்கு மேற்பட்ட முகவரி வழங்கப்பட்டால், ஒவ்வொரு சேவையகமும் ஒவ்வொன்றாக வெற்றிகரமாக பதிலளிக்கும் வரை, முதலில் இருந்து கடைசி வரை முயற்சிக்கப்படும். பதிலளிக்காத சேவையகங்கள் மீண்டும் ஆன்லைனில் வரும் வரை தற்காலிகமாகப் புறக்கணிக்கப்படும்.", "manage_concurrency": "ஒத்திசைவை நிர்வகிக்கவும்", "manage_log_settings": "பதிவு அமைப்புகளை நிர்வகிக்கவும்", "map_dark_style": "இருண்ட தீம்", @@ -150,6 +174,8 @@ "map_settings": "மேப் & ஜிபிஎஸ் (GPS) அமைப்புகள்", "map_settings_description": "மேப் அமைப்புகளை நிர்வகிக்கவும்", "map_style_description": "style.json மேப் தீமுக்கான URL", + "memory_cleanup_job": "நினைவகத்தை சுத்தம் செய்தல்", + "memory_generate_job": "நினைவக உருவாக்கம்", "metadata_extraction_job": "மெட்டாடேட்டாவைப் பிரித்தெடுக்கவும்", "metadata_extraction_job_description": "ஜிபிஎஸ் மற்றும் தெளிவுத்திறன் போன்ற ஒவ்வொரு சொத்திலிருந்தும் மெட்டாடேட்டா தகவலைப் பிரித்தெடுக்கவும்", "metadata_faces_import_setting": "முக இறக்குமதியை இயக்கவும்", @@ -158,12 +184,26 @@ "metadata_settings_description": "மேனிலை தரவு அமைப்புகளை நிர்வகிக்கவும்", "migration_job": "இடம்பெயர்தல்", "migration_job_description": "புகைப்படங்கள் மற்றும் முகங்களுக்கான சிறுபடங்களை (தம்ப்னெயில்) சமீபத்திய கோப்புறை அமைப்பிற்கு மாற்றவும்", + "nightly_tasks_cluster_faces_setting_description": "புதிதாகக் கண்டறியப்பட்ட முகங்களில் முக அங்கீகாரத்தை இயக்கு", + "nightly_tasks_cluster_new_faces_setting": "புதிய முகங்களைக் தொகுதிபடுத்து", + "nightly_tasks_database_cleanup_setting": "தரவுத்தளத்தை சுத்தம் செய்யும் பணிகள்", + "nightly_tasks_database_cleanup_setting_description": "தரவுத்தளத்திலிருந்து பழைய, காலாவதியான தரவை சுத்தம் செய்யவும்", + "nightly_tasks_generate_memories_setting": "நினைவுகளை உருவாக்கு", + "nightly_tasks_generate_memories_setting_description": "சொத்துக்களிலிருந்து புதிய நினைவுகளை உருவாக்கு", + "nightly_tasks_missing_thumbnails_setting": "விடுபட்ட சிறுபடங்களை உருவாக்கு", + "nightly_tasks_missing_thumbnails_setting_description": "சிறுபட உருவாக்கத்திற்காக சிறுபடங்கள் இல்லாமல் சொத்துக்களை வரிசைப்படுத்தவும்", + "nightly_tasks_settings": "இரவு நேரப் பணிகள் அமைப்புகள்", + "nightly_tasks_settings_description": "இரவு நேர பணிகளை நிர்வகி", + "nightly_tasks_start_time_setting": "தொடக்க நேரம்", + "nightly_tasks_start_time_setting_description": "சர்வர் இரவு நேர பணிகளை இயக்கத் தொடங்கும் நேரம்", + "nightly_tasks_sync_quota_usage_setting": "ஒத்திசைவு ஒதுக்கீடு பயன்பாடு", + "nightly_tasks_sync_quota_usage_setting_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_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் \". மின்னஞ்சல்களை அனுப்ப அனுமதிக்கப்பட்ட முகவரியைப் பயன்படுத்துவதை உறுதிசெய்து கொள்ளுங்கள்.", "notification_email_host_description": "மின்னஞ்சல் சேவையகத்தின் ஹோஸ்ட் (எடுத்துக்காட்டாக: smtp.immich.app)", "notification_email_ignore_certificate_errors": "சான்றிதழ் பிழைகளை புறக்கணிக்கவும்", "notification_email_ignore_certificate_errors_description": "TLS சான்றிதழ் சரிபார்ப்பு பிழைகளை புறக்கணிக்கவும் (பரிந்துரைக்கப்படவில்லை)", @@ -183,11 +223,14 @@ "oauth_auto_register": "தானியங்கு பதிவு", "oauth_auto_register_description": "OAuth உடன் உள்நுழைந்த பிறகு தானாகவே புதிய பயனர்களைப் பதிவுசெய்யவும்", "oauth_button_text": "பட்டன் உரை", + "oauth_client_secret_description": "அவசியம், OAuth வழங்குநரால் PKCE (குறியீட்டுப் பரிமாற்றத்திற்கான ஆதார விசை) ஆதரிக்கப்படாவிட்டால்", "oauth_enable_description": "OAuth மூலம் உள்நுழைக", "oauth_mobile_redirect_uri": "மொபைல் வழிமாற்று URI", "oauth_mobile_redirect_uri_override": "மொபைல் வழிமாற்று URI மேலெழுதுதல்", - "oauth_mobile_redirect_uri_override_description": "'app.immich:/' தவறான வழிமாற்று URI ஆக இருக்கும்போது இயக்கவும்.", - "oauth_settings": "Oauth", + "oauth_mobile_redirect_uri_override_description": "''{callback}'' போன்ற மொபைல் URI ஐ OAuth வழங்குநர் அனுமதிக்காதபோது இயக்கவும்", + "oauth_role_claim": "பதவி உரிமைகோரல்", + "oauth_role_claim_description": "இந்தக் கோரிக்கையின் இருப்பின் அடிப்படையில் தானாகவே நிர்வாகி அணுகலை வழங்கவும். கோரிக்கையில் 'பயனர்' அல்லது 'நிர்வாகி' இருக்கலாம்.", + "oauth_settings": "ஓஆத்", "oauth_settings_description": "OAuth உள்நுழைவு அமைப்புகளை நிர்வகிக்கவும்", "oauth_settings_more_details": "இந்த அம்சத்தைப் பற்றிய கூடுதல் விவரங்களுக்கு, டாக்ஸ் ஐப் பார்க்கவும்.", "oauth_storage_label_claim": "சேமிப்பக லேபிள் உரிமைகோரல்", @@ -195,7 +238,9 @@ "oauth_storage_quota_claim": "சேமிப்பக ஒதுக்கீடு உரிமைகோரல்", "oauth_storage_quota_claim_description": "இந்த உரிமைகோரலின் மதிப்பிற்கு பயனரின் சேமிப்பக ஒதுக்கீட்டை தானாக அமைக்கவும்.", "oauth_storage_quota_default": "இயல்புநிலை சேமிப்பக ஒதுக்கீடு (GiB)", - "oauth_storage_quota_default_description": "GiB இல் உள்ள ஒதுக்கீடு எந்த உரிமைகோரலும் வழங்கப்படாதபோது பயன்படுத்தப்படும் (வரம்பற்ற ஒதுக்கீட்டிற்கு 0 ஐ உள்ளிடவும்).", + "oauth_storage_quota_default_description": "GiB இல் உள்ள ஒதுக்கீடு எந்த உரிமைகோரலும் வழங்கப்படாதபோது பயன்படுத்தப்படும் .", + "oauth_timeout": "கோரிக்கை நேரம் முடிந்தது", + "oauth_timeout_description": "கோரிக்கைகளுக்கான காலக்கெடு மில்லி வினாடிகளில்", "password_enable_description": "மின்னஞ்சல் மற்றும் கடவுச்சொல் மூலம் உள்நுழையவும்", "password_settings": "கடவுச்சொல் உள்நுழைவு", "password_settings_description": "கடவுச்சொல் உள்நுழைவு அமைப்புகளை நிர்வகிக்கவும்", @@ -209,7 +254,7 @@ "reset_settings_to_default": "அமைப்புகளை இயல்புநிலைக்கு மீட்டமைக்கவும்", "reset_settings_to_recent_saved": "அண்மையில் சேமிக்கப்பட்ட அமைப்புகளுக்கு அமைப்புகளை மீட்டமைக்கவும்", "scanning_library": "ச்கேனிங் நூலகம்", - "search_jobs": "வேலைகளைத் தேடுங்கள் ...", + "search_jobs": "வேலைகளைத் தேடுங்கள்…", "send_welcome_email": "வரவேற்பு மின்னஞ்சலை அனுப்பவும்", "server_external_domain_settings": "வெளிப்புற களம்", "server_external_domain_settings_description": "HTTP (கள்) உட்பட பொது பகிரப்பட்ட இணைப்புகளுக்கான டொமைன்: //", @@ -230,9 +275,10 @@ "storage_template_hash_verification_enabled_description": "ஹாஷ் சரிபார்ப்பை இயக்குகிறது, தாக்கங்கள் குறித்து உறுதியாக தெரியாவிட்டால் இதை முடக்க வேண்டாம்", "storage_template_migration": "ஸ்டோரேஜ் டெம்ப்ளேட் இடம்பெயர்வு", "storage_template_migration_description": "ஏற்கனவே பதிவேற்றிய புகைப்படங்களுக்கு தற்போதைய {template} ஐப் பயன்படுத்தவும்", - "storage_template_migration_info": "டெம்ப்ளேட் மாற்றங்கள் புதிய படங்களுக்கு மட்டுமே பொருந்தும். முன்பு பதிவேற்றிய படங்களுக்கு டெம்ப்ளேட்டைப் பயன்படுத்த, {job} ஐ இயக்கவும்.", + "storage_template_migration_info": "சேமிப்பக வார்ப்புரு அனைத்து நீட்டிப்புகளையும் சிறிய எழுத்துக்களுக்கு மாற்றும். டெம்ப்ளேட் மாற்றங்கள் புதிய படங்களுக்கு மட்டுமே பொருந்தும். முன்பு பதிவேற்றிய படங்களுக்கு டெம்ப்ளேட்டைப் பயன்படுத்த, {job} ஐ இயக்கவும்.", "storage_template_migration_job": "ஸ்டோரேஜ் டெம்ப்ளேட் இடம்பெயர்வு வேலை", "storage_template_more_details": "இந்த அம்சத்தைப் பற்றிய கூடுதல் விவரங்களுக்கு, Storage Template மற்றும் அதன் தாக்கங்கள் ஐப் பார்க்கவும்", + "storage_template_onboarding_description_v2": "இயக்கப்பட்டால், இந்த அம்சம் பயனர் வரையறுக்கப்பட்ட டெம்ப்ளேட்டின் அடிப்படையில் கோப்புகளை தானாக ஒழுங்கமைக்கும். மேலும் தகவலுக்கு, ஆவணங்கள் ஐப் பார்க்கவும்.", "storage_template_path_length": "தோராயமான பாதை நீள வரம்பு: {length, number}/{limit, number}", "storage_template_settings": "ஸ்டோரேஜ் டெம்ப்ளேட்", "storage_template_settings_description": "பதிவேற்ற புகைப்படங்களின் கோப்புறை அமைப்பு மற்றும் கோப்பு பெயரை நிர்வகிக்கவும்", @@ -247,7 +293,7 @@ "template_email_update_album": "ஆல்பம் வார்ப்புருவைப் புதுப்பிக்கவும்", "template_email_welcome": "மின்னஞ்சல் வார்ப்புருவை வரவேற்கிறோம்", "template_settings": "அறிவிப்பு வார்ப்புருக்கள்", - "template_settings_description": "அறிவிப்புகளுக்கு தனிப்பயன் வார்ப்புருக்கள் நிர்வகிக்கவும்.", + "template_settings_description": "அறிவிப்புகளுக்கு தனிப்பயன் வார்ப்புருக்கள் நிர்வகிக்கவும்", "theme_custom_css_settings": "தனிப்பயன் CSS", "theme_custom_css_settings_description": "CSS அம்சம் Immich வடிவமைப்பை தனிப்பயனாக்க அனுமதிக்கிறது.", "theme_settings": "தீம் அமைப்புகள்", @@ -276,8 +322,10 @@ "transcoding_constant_rate_factor": "நிலையான வீத காரணி (-crf)", "transcoding_constant_rate_factor_description": "வீடியோ தர நிலை. வழக்கமான மதிப்புகள் H.264 க்கு 23, HEVC க்கு 28, VP9 க்கு 31, மற்றும் AV1 க்கு 35 ஆகும். குறைவானது சிறந்தது, ஆனால் பெரிய கோப்புகளை உருவாக்குகிறது.", "transcoding_disabled_description": "எந்த வீடியோக்களையும் டிரான்ச்கோட் செய்யாதீர்கள், சில வாடிக்கையாளர்களின் பிளேபேக்கை உடைக்கலாம்", + "transcoding_encoding_options": "குறியீட்டு விருப்பங்கள்", + "transcoding_encoding_options_description": "குறியிடப்பட்ட வீடியோக்களுக்கான கோடெக்குகள், தெளிவுத்திறன், தரம் மற்றும் பிற விருப்பங்களை அமைக்கவும்", "transcoding_hardware_acceleration": "வன்பொருள் முடுக்கம்", - "transcoding_hardware_acceleration_description": "சோதனை; மிக வேகமாக, ஆனால் அதே பிட்ரேட்டில் குறைந்த தகுதி இருக்கும்", + "transcoding_hardware_acceleration_description": "பரிசோதனை: வேகமான டிரான்ஸ்கோடிங் ஆனால் அதே பிட்ரேட்டில் தரத்தைக் குறைக்கலாம்.", "transcoding_hardware_decoding": "வன்பொருள் டிகோடிங்", "transcoding_hardware_decoding_setting_description": "குறியாக்கத்தை விரைவுபடுத்துவதற்கு பதிலாக இறுதி முதல் இறுதி முடுக்கம் ஆகியவற்றை செயல்படுத்துகிறது. எல்லா வீடியோக்களிலும் வேலை செய்யக்கூடாது.", "transcoding_max_b_frames": "அதிகபட்ச பி-பிரேம்கள்", @@ -287,6 +335,8 @@ "transcoding_max_keyframe_interval": "அதிகபட்ச கீஃப்ரேம் இடைவெளி", "transcoding_max_keyframe_interval_description": "கீஃப்ரேம்களுக்கு இடையில் அதிகபட்ச பிரேம் தூரத்தை அமைக்கிறது. குறைந்த மதிப்புகள் சுருக்க செயல்திறனை மோசமாக்குகின்றன, ஆனால் தேடல் நேரங்களை மேம்படுத்துகின்றன, மேலும் வேகமான இயக்கத்துடன் காட்சிகளில் தரத்தை மேம்படுத்தலாம். 0 இந்த மதிப்பை தானாக அமைக்கிறது.", "transcoding_optimal_description": "இலக்கு தீர்மானத்தை விட உயர்ந்த வீடியோக்கள் அல்லது ஏற்றுக்கொள்ளப்பட்ட வடிவத்தில் இல்லை", + "transcoding_policy": "குறிமாற்றக் கொள்கை", + "transcoding_policy_description": "ஒரு வீடியோ எப்போது குறிமாற்றம் செய்யப்படும் என்பதை அமைக்கவும்", "transcoding_preferred_hardware_device": "விருப்பமான வன்பொருள் சாதனம்", "transcoding_preferred_hardware_device_description": "VAAPI மற்றும் QSV க்கு மட்டுமே பொருந்தும். வன்பொருள் டிரான்ச்கோடிங்கிற்கு பயன்படுத்தப்படும் ட்ரை முனையை அமைக்கிறது.", "transcoding_preset_preset": "முன்னமைக்கப்பட்ட (-பிரசெட்)", @@ -295,6 +345,7 @@ "transcoding_reference_frames_description": "கொடுக்கப்பட்ட சட்டகத்தை சுருக்கும்போது குறிப்பிட வேண்டிய பிரேம்களின் எண்ணிக்கை. அதிக மதிப்புகள் சுருக்க செயல்திறனை மேம்படுத்துகின்றன, ஆனால் குறியாக்கத்தை மெதுவாக்குகின்றன. 0 இந்த மதிப்பை தானாக அமைக்கிறது.", "transcoding_required_description": "ஏற்றுக்கொள்ளப்பட்ட வடிவத்தில் இல்லாத வீடியோக்கள் மட்டுமே", "transcoding_settings": "வீடியோ டிரான்ச்கோடிங் அமைப்புகள்", + "transcoding_settings_description": "எந்த வீடியோக்களை டிரான்ஸ்கோட் செய்ய வேண்டும், அவற்றை எவ்வாறு செயலாக்க வேண்டும் என்பதை நிர்வகிக்கவும்", "transcoding_target_resolution": "இலக்கு தீர்மானம்", "transcoding_target_resolution_description": "அதிக தீர்மானங்கள் அதிக விவரங்களை பாதுகாக்க முடியும், ஆனால் குறியாக்க அதிக நேரம் எடுக்கும், பெரிய கோப்பு அளவுகளைக் கொண்டிருக்கலாம், மேலும் பயன்பாட்டு மறுமொழியைக் குறைக்கலாம்.", "transcoding_temporal_aq": "தம்போர்ல்", @@ -307,23 +358,28 @@ "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": "நாட்களின் எண்ணிக்கை", "trash_number_of_days_description": "சொத்துக்களை நிரந்தரமாக அகற்றுவதற்கு முன் குப்பைத்தொட்டியில் வைத்திருக்க நாட்கள் எண்ணிக்கை", "trash_settings": "குப்பை அமைப்புகள்", "trash_settings_description": "குப்பை அமைப்புகளை நிர்வகிக்கவும்", + "unlink_all_oauth_accounts": "அனைத்து OAuth கணக்குகளின் இணைப்பையும் நீக்கு", + "unlink_all_oauth_accounts_description": "புதிய வழங்குநருக்கு மாறுவதற்கு முன், அனைத்து OAuth கணக்குகளின் இணைப்பையும் நீக்க நினைவில் கொள்ளுங்கள்.", + "unlink_all_oauth_accounts_prompt": "எல்லா OAuth கணக்குகளின் இணைப்பையும் நீக்க விரும்புகிறீர்களா? இது ஒவ்வொரு பயனருக்கும் OAuth ஐடியை மீட்டமைக்கும், மேலும் இதை திரும்பப் பெறு முடியாது.", "user_cleanup_job": "பயனர் தூய்மைப்படுத்துதல்", - "user_delete_delay": " {user} இன் கணக்கு மற்றும் சொத்துக்கள் {தாமதம், பன்மை, ஒன்று {# நாள்} மற்ற {# நாட்கள்}} இல் நிரந்தர நீக்க திட்டமிடப்படும்.", + "user_delete_delay": "{user}இன் கணக்கு மற்றும் சொத்துக்கள் {delay, plural, one {# நாள்} other {# நாள்கள்}}இல் நிரந்தர நீக்கத் திட்டமிடப்படும்.", "user_delete_delay_settings": "தாமதத்தை நீக்கு", "user_delete_delay_settings_description": "எண் of days after நீக்கும் பெறுநர் permanently நீக்கு a user's account and assets. நீக்குவதற்கு தயாராக இருக்கும் பயனர்களைச் சரிபார்க்க பயனர் நீக்குதல் வேலை நள்ளிரவில் இயங்குகிறது. இந்த அமைப்பில் மாற்றங்கள் அடுத்த மரணதண்டனையில் மதிப்பீடு செய்யப்படும்.", "user_delete_immediately": " {user} இன் கணக்கு மற்றும் சொத்துக்கள் நிரந்தர நீக்குதலுக்காக வரிசையில் நிற்கப்படும் உடனடியாக .", "user_delete_immediately_checkbox": "உடனடியாக நீக்க பயனர் மற்றும் சொத்துக்கள்", + "user_details": "பயனர் விவரங்கள்", "user_management": "பயனர் மேலாண்மை", "user_password_has_been_reset": "பயனரின் கடவுச்சொல் மீட்டமைக்கப்பட்டுள்ளது:", "user_password_reset_description": "தயவுசெய்து தற்காலிக கடவுச்சொல்லை பயனருக்கு வழங்கவும், அவர்களின் அடுத்த உள்நுழைவில் கடவுச்சொல்லை மாற்ற வேண்டும் என்று அவர்களுக்குத் தெரிவிக்கவும்.", "user_restore_description": " {user} இன் கணக்கு மீட்டெடுக்கப்படும்.", - "user_restore_scheduled_removal": "பயனரை மீட்டமை - {தேதி, தேதி, நீண்ட} இல் திட்டமிடப்பட்ட நீக்குதல்", + "user_restore_scheduled_removal": "பயனரை மீட்டமை - {date, date, long} இல் திட்டமிடப்பட்ட நீக்குதல்", "user_settings": "பயனர் அமைப்புகள்", "user_settings_description": "பயனர் அமைப்புகளை நிர்வகிக்கவும்", "user_successfully_removed": "பயனர் {email} வெற்றிகரமாக அகற்றப்பட்டது.", @@ -338,29 +394,62 @@ "admin_password": "நிர்வாகி கடவுச்சொல்", "administration": "நிர்வாகம்", "advanced": "மேம்பட்ட", - "age_months": "அகவை {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}}", - "age_year_months": "அகவை 1 அகவை, {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}}", - "age_years": "{ஆண்டுகள், பன்மை, பிற {வயது #}}", + "advanced_settings_enable_alternate_media_filter_subtitle": "மாற்று அளவுகோல்களின் அடிப்படையில் ஒத்திசைவின் போது மீடியாவை வடிகட்ட இந்த விருப்பத்தைப் பயன்படுத்தவும். எல்லா ஆல்பங்களையும் ஆப்ஸ் கண்டறிவதில் சிக்கல்கள் இருந்தால் மட்டுமே இதை முயற்சிக்கவும்.", + "advanced_settings_enable_alternate_media_filter_title": "[பரிசோதனைக்கு உட்பட்டது] மாற்று சாதன ஆல்ப ஒத்திசைவு வடிப்பானைப் பயன்படுத்தவும்", + "advanced_settings_log_level_title": "பதிவு நிலை: {level}", + "advanced_settings_prefer_remote_subtitle": "சில சாதனங்கள் உள் சொத்துக்களிலிருந்து சிறுபடங்களை ஏற்றுவதில் மிகவும் மெதுவாக இருக்கும். அதற்கு பதிலாக சர்வர் படங்களை ஏற்ற இந்த அமைப்பைச் செயல்படுத்தவும்.", + "advanced_settings_prefer_remote_title": "ரிமோட் படங்களுக்கு முன்னுரிமை கொடு", + "advanced_settings_proxy_headers_subtitle": "ஒவ்வொரு நெட்வொர்க் கோரிக்கையுடனும் இம்மிச் அனுப்ப வேண்டிய ப்ராக்ஸி தலைப்புகளை வரையறுக்கவும்", + "advanced_settings_proxy_headers_title": "ப்ராக்ஸி தலைப்புகள்", + "advanced_settings_readonly_mode_subtitle": "புகைப்படங்களை பார்ப்பதற்கு மட்டுமே அனுமதிக்கும் படிப்பதற்கு மட்டும் பயன்முறையை இயக்குகிறது, பல படங்களைத் தேர்ந்தெடுப்பது, பகிர்தல், அனுப்புதல், நீக்குதல் போன்ற அனைத்தும் முடக்கப்பட்டுள்ளன. பிரதான திரையில் இருந்து பயனர் அவதார் வழியாக படிக்க மட்டும் என்பதை இயக்கு/முடக்கு", + "advanced_settings_readonly_mode_title": "படிக்க மட்டுமேயான பயன்முறை", + "advanced_settings_self_signed_ssl_subtitle": "சர்வர் எண்ட்பாயிண்டிற்கான SSL சான்றிதழ் சரிபார்ப்பை தவிர்க்கிறது. சுய கையொப்பமிட்ட சான்றிதழ்களுக்கு இது தேவையானது.", + "advanced_settings_self_signed_ssl_title": "சுய கையொப்பமிட்ட SSL சான்றிதழ்களை அனுமதி", + "advanced_settings_sync_remote_deletions_subtitle": "இணையத்தில் நடவடிக்கை எடுக்கப்படும்போது, இந்தச் சாதனத்தில் உள்ள ஒரு சொத்தை தானாகவே நீக்கவும் அல்லது மீட்டெடுக்கவும்", + "advanced_settings_sync_remote_deletions_title": "தொலைநிலை நீக்குதல்களை ஒத்திசைக்கவும் [பரிசோதனைக்கு உட்பட்டது]", + "advanced_settings_tile_subtitle": "மேம்பட்ட பயனர் அமைப்புகள்", + "advanced_settings_troubleshooting_subtitle": "சரிசெய்தலுக்கு கூடுதல் அம்சங்களை இயக்கவும்", + "advanced_settings_troubleshooting_title": "சரிசெய்தல்", + "age_months": "அகவை {months, plural, one {# திங்கள்} other {# திங்கள்கள்}}", + "age_year_months": "அகவை 1 ஆண்டு, {months, plural, one {# திங்கள்} other {# திங்கள்கள்}}", + "age_years": "{years, plural, other {அகவை #}}", "album_added": "ஆல்பம் சேர்க்கப்பட்டது", "album_added_notification_setting_description": "பகிரப்பட்ட ஆல்பத்தில் நீங்கள் சேர்க்கப்படும்போது மின்னஞ்சல் அறிவிப்பைப் பெறுங்கள்", "album_cover_updated": "ஆல்பம் கவர் புதுப்பிக்கப்பட்டது", "album_delete_confirmation": "{album} ஆல்பத்தை நீக்க விரும்புகிறீர்களா?", "album_delete_confirmation_description": "இந்த ஆல்பம் பகிரப்பட்டால், மற்ற பயனர்களால் இதை அணுக முடியாது.", + "album_deleted": "ஆல்பம் நீக்கப்பட்டது", + "album_info_card_backup_album_excluded": "விலக்கப்பட்டது", + "album_info_card_backup_album_included": "சேர்க்கப்பட்டுள்ளது", "album_info_updated": "ஆல்பம் செய்தி புதுப்பிக்கப்பட்டது", "album_leave": "ஆல்பத்தை விடுங்கள்?", - "album_leave_confirmation": "நீங்கள் நிச்சயமாக {ஆல்பத்தை விட்டு வெளியேற விரும்புகிறீர்களா?", + "album_leave_confirmation": "நீங்கள் நிச்சயமாக {album}ஐ விட்டு வெளியேற விரும்புகிறீர்களா?", "album_name": "ஆல்பத்தின் பெயர்", "album_options": "ஆல்பம் விருப்பங்கள்", "album_remove_user": "பயனரை அகற்றவா?", "album_remove_user_confirmation": "{user} ஐ அகற்ற விரும்புகிறீர்களா?", + "album_search_not_found": "உங்கள் தேடலுடன் பொருந்தக்கூடிய ஆல்பங்கள் எதுவும் இல்லை", "album_share_no_users": "இந்த ஆல்பத்தை நீங்கள் எல்லா பயனர்களுடனும் பகிர்ந்து கொண்டதாகத் தெரிகிறது அல்லது பகிர்வதற்கு உங்களிடம் எந்த பயனரும் இல்லை.", + "album_summary": "ஆல்பம் சுருக்கம்", "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_leave": "ஆல்பம் விட்டு வெளியேற முடியவில்லை", + "album_viewer_appbar_share_err_remove": "ஆல்பத்திலிருந்து சொத்துக்களை அகற்றுவதில் சிக்கல்கள் உள்ளன", + "album_viewer_appbar_share_err_title": "ஆல்பத்தின் தலைப்பை மாற்ற முடியவில்லை", + "album_viewer_appbar_share_leave": "ஆல்பத்தை விட்டு வெளியேறு", + "album_viewer_appbar_share_to": "பகிரு", + "album_viewer_page_share_add_users": "பயனர்களைச் சேர்க்கவும்", "album_with_link_access": "இணைப்பு உள்ள எவரும் இந்த ஆல்பத்தில் புகைப்படங்களையும் நபர்களையும் பார்க்கட்டும்.", "albums": "ஆல்பம்", - "albums_count": "{எண்ணிக்கை, பன்மை, ஒன்று {{எண்ணிக்கை, எண்} ஆல்பம்} பிற {{எண்ணிக்கை, எண்} ஆல்பங்கள்}}", + "albums_count": "{count, plural, one {{count, number} தொகுப்பு} other {{count, number} தொகுப்புகள்}}", + "albums_default_sort_order": "இயல்புநிலை ஆல்பம் வரிசையாக்கம்", + "albums_default_sort_order_description": "புதிய ஆல்பங்களை உருவாக்கும்போது ஆரம்ப சொத்து வரிசைப்படுத்தல் வரிசை.", + "albums_feature_description": "பிற பயனர்களுடன் பகிர்ந்து கொள்ளக்கூடிய சொத்துக்களின் தொகுப்புகள்.", + "albums_on_device_count": "சாதனத்தில் ஆல்பங்கள் ({count})", "all": "அனைத்தும்", "all_albums": "அனைத்து ஆல்பங்களும்", "all_people": "அனைத்து மக்களும்", @@ -369,47 +458,160 @@ "allow_edits": "திருத்தங்களை அனுமதிக்கவும்", "allow_public_user_to_download": "பொது பயனரை பதிவிறக்கம் செய்ய அனுமதிக்கவும்", "allow_public_user_to_upload": "பொது பயனரை பதிவேற்ற அனுமதிக்கவும்", + "alt_text_qr_code": "QR குறியீடு படம்", "anti_clockwise": "கடிகார எதிர்ப்பு", "api_key": "பநிஇ விசை", "api_key_description": "இந்த மதிப்பு ஒரு முறை மட்டுமே காண்பிக்கப்படும். சாளரத்தை மூடுவதற்கு முன் அதை நகலெடுக்க மறக்காதீர்கள்.", "api_key_empty": "உங்கள் பநிஇ விசை பெயர் காலியாக இருக்கக்கூடாது", "api_keys": "பநிஇ விசைகள்", + "app_bar_signout_dialog_content": "நீங்கள் நிச்சயமாக வெளியேற விரும்புகிறீர்களா?", + "app_bar_signout_dialog_ok": "ஆம்", + "app_bar_signout_dialog_title": "வெளியேறு", "app_settings": "பயன்பாட்டு அமைப்புகள்", "appears_in": "தோன்றும்", + "apply_count": "போடு ({count, number})", "archive": "காப்பகம்", + "archive_action_prompt": "{count} காப்பகத்தில் சேர்க்கப்பட்டது", "archive_or_unarchive_photo": "காப்பகம் அல்லது செயலற்ற புகைப்படம்", + "archive_page_no_archived_assets": "காப்பகப்படுத்தப்பட்ட சொத்துக்கள் எதுவும் கிடைக்கவில்லை", + "archive_page_title": "காப்பகம் ({count})", "archive_size": "காப்பக அளவு", "archive_size_description": "பதிவிறக்கங்களுக்கான காப்பக அளவை உள்ளமைக்கவும் (கிபில்)", - "archived_count": "{எண்ணிக்கை, பன்மை, பிற {காப்பகப்படுத்தப்பட்ட #}}", + "archived": "காப்பகப்படுத்தப்பட்டது", + "archived_count": "{count, plural, other {காப்பகப்படுத்தப்பட்டது #}}", "are_these_the_same_person": "இவர்கள் ஒரே நபரா?", "are_you_sure_to_do_this": "இதை நீங்கள் செய்ய விரும்புகிறீர்களா?", + "asset_action_delete_err_read_only": "சொத்து (களை) மட்டுமே படிக்க முடியாது", + "asset_action_share_err_offline": "இணைப்பில்லாத சொத்து (களை) பெற முடியாது, தவிர்க்கவும்", "asset_added_to_album": "ஆல்பத்தில் சேர்க்கப்பட்டது", - "asset_adding_to_album": "ஆல்பத்தில் சேர்க்கிறது ...", + "asset_adding_to_album": "ஆல்பத்தில் சேர்க்கிறது…", "asset_description_updated": "சொத்து விளக்கம் புதுப்பிக்கப்பட்டுள்ளது", "asset_filename_is_offline": "சொத்து {filename} ஆஃப்லைனில் உள்ளது", "asset_has_unassigned_faces": "சொத்து ஒதுக்கப்படாத முகங்களைக் கொண்டுள்ளது", - "asset_hashing": "ஏசிங் ...", + "asset_hashing": "ஏசிங்…", + "asset_list_group_by_sub_title": "குழு", + "asset_list_layout_settings_dynamic_layout_title": "மாறும் தளவமைப்பு", + "asset_list_layout_settings_group_automatically": "தானியங்கி", + "asset_list_layout_settings_group_by": "மூலம் குழு சொத்துக்கள்", + "asset_list_layout_settings_group_by_month_day": "மாதம் + நாள்", + "asset_list_layout_sub_title": "மனையமைவு", + "asset_list_settings_subtitle": "புகைப்பட கட்டம் தளவமைப்பு அமைப்புகள்", + "asset_list_settings_title": "புகைப்பட கட்டம்", "asset_offline": "சொத்து ஆஃப்லைனில்", "asset_offline_description": "இந்த வெளிப்புற சொத்து இனி வட்டில் காணப்படவில்லை. உதவிக்கு உங்கள் இம்மிச் நிர்வாகியை தொடர்பு கொள்ளவும்.", + "asset_restored_successfully": "சொத்து வெற்றிகரமாக மீட்டெடுக்கப்பட்டது", "asset_skipped": "தவிர்க்கப்பட்டது", "asset_skipped_in_trash": "குப்பையில்", + "asset_trashed": "சொத்து குப்பை", + "asset_troubleshoot": "சொத்து சரிசெய்தல்", "asset_uploaded": "பதிவேற்றப்பட்டது", - "asset_uploading": "பதிவேற்றுதல் ...", + "asset_uploading": "பதிவேற்றுதல்…", + "asset_viewer_settings_subtitle": "உங்கள் கேலரி பார்வையாளர் அமைப்புகளை நிர்வகிக்கவும்", + "asset_viewer_settings_title": "சொத்து பார்வையாளர்", "assets": "சொத்துக்கள்", - "assets_added_count": "சேர்க்கப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", - "assets_added_to_album_count": "ஆல்பத்தில் {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", - "assets_count": "{எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", - "assets_moved_to_trash_count": "நகர்த்தப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}}", - "assets_permanently_deleted_count": "நிரந்தரமாக நீக்கப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்}}", - "assets_removed_count": "அகற்றப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", + "assets_added_count": "சேர்க்கப்பட்டது {count, plural, one {# சொத்து} other {# சொத்துக்கள்}}", + "assets_added_to_album_count": "தொகுப்பில் {count, plural, one {# சொத்து} other {# சொத்துக்கள்}} சேர்க்கப்பட்டது", + "assets_added_to_albums_count": "Added {assetTotal, plural, one {# சொத்து} other {# சொத்துக்கள்}} to {albumTotal, plural, one {# தொகுப்பு} other {# தொகுப்புகள்}}சேர்க்கப்பட்டது", + "assets_cannot_be_added_to_album_count": "{count, plural, one {சொத்து} other {சொத்துக்கள்}} செருகேட்டில் சேர்க்க முடியாது", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} எந்தச் செருகேட்டிலும் சேர்க்க முடியாது", + "assets_count": "{count, plural, one {# சொத்து} other {# சொத்துக்கள்}}", + "assets_deleted_permanently": "{count} சொத்து (கள்) நிரந்தரமாக நீக்கப்பட்டது", + "assets_deleted_permanently_from_server": "{count} சொத்து (கள்) இம்மிச் சேவையகத்திலிருந்து நிரந்தரமாக நீக்கப்பட்டது", + "assets_downloaded_failed": "{count, plural, one {# கோப்பு பதிவிறக்கப்பட்டது - {error} கோப்பு தோல்வியடைந்தது} other {# கோப்புகள் பதிவிறக்கப்பட்டன - {error} கோப்புகள் தோல்வியடைந்தன}}", + "assets_downloaded_successfully": "{count, plural, one {# கோப்பு வெற்றிகரமாகப் பதிவிறக்கம்} other {# கோப்புகள் வெற்றிகரமாகப் பதிவிறக்கம்}}", + "assets_moved_to_trash_count": "குப்பைக்கு {count, plural, one {# சொத்து} other {# சொத்துக்கள்}} நகர்த்தப்பட்டது", + "assets_permanently_deleted_count": "{count, plural, one {# சொத்து} other {# சொத்துக்கள்}} நிரந்தரமாக நீக்கப்பட்டது", + "assets_removed_count": "{count, plural, one {# சொத்து} other {# சொத்துக்கள்}}அகற்றப்பட்டது", + "assets_removed_permanently_from_device": "{count} சொத்து (கள்) உங்கள் சாதனத்திலிருந்து நிரந்தரமாக அகற்றப்பட்டது", "assets_restore_confirmation": "உங்கள் குப்பைத் தொட்டிகள் அனைத்தையும் மீட்டெடுக்க விரும்புகிறீர்களா? இந்த செயலை நீங்கள் செயல்தவிர்க்க முடியாது! எந்தவொரு இணைப்பில்லாத சொத்துக்களையும் இந்த வழியில் மீட்டெடுக்க முடியாது என்பதை நினைவில் கொள்க.", - "assets_restored_count": "மீட்டெடுக்கப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", - "assets_trashed_count": "குப்பைத்தொட்டியான {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", - "assets_were_part_of_album_count": "{எண்ணிக்கை, பன்மை, ஒரு {Asset was} மற்ற {Assets were}} ஏற்கனவே ஆல்பத்தின் ஒரு பகுதி", + "assets_restored_count": "{count, plural, one {# சொத்து} other {# சொத்துக்கள்}}மீட்டெடுக்கப்பட்டது", + "assets_restored_successfully": "{count} சொத்து (கள்) வெற்றிகரமாக மீட்டெடுக்கப்பட்டது", + "assets_trashed": "{count} சொத்து (கள்) குப்பை", + "assets_trashed_count": "{count, plural, one {# சொத்து} other {# சொத்துக்கள்}} குப்பையாகப்பட்டது", + "assets_trashed_from_server": "{count} சொத்து (கள்) இம்மிச் சேவையகத்திலிருந்து குப்பைத் தொட்டியில்", + "assets_were_part_of_album_count": "{count, plural, one {சொத்து} other {சொத்துக்கள்}} ஏற்கனவே தொகுப்பின் ஒரு பகுதி", + "assets_were_part_of_albums_count": "{count, plural, one {சொத்து} other {சொத்துக்கள்}} ஏற்கனவே தொகுப்புகளின் ஒரு பகுதி", "authorized_devices": "அங்கீகரிக்கப்பட்ட சாதனங்கள்", + "automatic_endpoint_switching_subtitle": "கிடைக்கும்போது நியமிக்கப்பட்ட வைஃபை மீது உள்ளூரில் இணைக்கவும், வேறு இடங்களில் மாற்று இணைப்புகளைப் பயன்படுத்தவும்", + "automatic_endpoint_switching_title": "தானியங்கி முகவரி மாறுதல்", + "autoplay_slideshow": "ஆட்டோபிளே ச்லைடுசோ", "back": "பின்", "back_close_deselect": "பின், மூடு அல்லது தேர்வுநீக்கம்", + "background_backup_running_error": "பின்னணி காப்புப்பிரதி தற்போது இயங்குகிறது, கைமுறை காப்புப்பிரதியைத் தொடங்க முடியாது", + "background_location_permission": "பின்னணி இருப்பிட இசைவு", + "background_location_permission_content": "பின்னணியில் இயங்கும் போது நெட்வொர்க்குகளை மாற்ற, இம்மிச் *எப்போதும்* துல்லியமான இருப்பிட அணுகலைக் கொண்டிருக்க வேண்டும், எனவே பயன்பாடு வைஃபை நெட்வொர்க்கின் பெயரைப் படிக்க முடியும்", + "background_options": "பின்னணி விருப்பங்கள்", + "backup": "காப்புப்பிரதி", + "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": "ஆல்பங்களைத் தேர்ந்தெடுக்கவும்", + "backup_album_selection_page_selection_info": "தேர்வு செய்தி", + "backup_album_selection_page_total_assets": "மொத்த தனித்துவமான சொத்துக்கள்", + "backup_albums_sync": "காப்புப்பிரதி ஆல்பங்கள் ஒத்திசைவு", + "backup_all": "அனைத்தும்", + "backup_background_service_backup_failed_message": "சொத்துக்களை காப்புப்பிரதி எடுக்கத் தவறிவிட்டது. மீண்டும் முயற்சிப்பது…", + "backup_background_service_connection_failed_message": "சேவையகத்துடன் இணைக்கத் தவறிவிட்டது. மீண்டும் முயற்சிப்பது…", + "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": "{filename} பதிவேற்றுவதில் தோல்வி", + "backup_controller_page_albums": "காப்புப்பிரதி ஆல்பங்கள்", + "backup_controller_page_background_app_refresh_disabled_content": "பின்னணி காப்புப்பிரதியைப் பயன்படுத்துவதற்காக அமைப்புகளில் பின்னணி பயன்பாட்டு புதுப்பிப்புகளை இயக்கவும்> பொது> பின்னணி பயன்பாட்டு புதுப்பிப்பு.", + "backup_controller_page_background_app_refresh_disabled_title": "பின்னணி பயன்பாட்டு புதுப்பிப்பு முடக்கப்பட்டது", + "backup_controller_page_background_app_refresh_enable_button_text": "அமைப்புகளுக்குச் செல்லுங்கள்", + "backup_controller_page_background_battery_info_link": "எப்படி என்று எனக்குக் காட்டு", + "backup_controller_page_background_battery_info_message": "சிறந்த பின்னணி காப்புப்பிரதி அனுபவத்திற்கு, இம்மிச்சிற்கான பின்னணி செயல்பாட்டைக் கட்டுப்படுத்தும் எந்த பேட்டரி மேம்படுத்தல்களையும் முடக்கவும். \n\nஇது சாதனம் சார்ந்ததாக இருப்பதால், உங்கள் சாதன உற்பத்தியாளருக்கு தேவையான தகவல்களைத் தேடுங்கள்.", + "backup_controller_page_background_battery_info_ok": "சரி", + "backup_controller_page_background_battery_info_title": "பேட்டரி மேம்படுத்தல்கள்", + "backup_controller_page_background_charging": "கட்டணம் வசூலிக்கும் போது மட்டுமே", + "backup_controller_page_background_configure_error": "பின்னணி சேவையை உள்ளமைக்கத் தவறிவிட்டது", + "backup_controller_page_background_delay": "புதிய சொத்துக்களை தாமதப்படுத்துங்கள்: {duration}", + "backup_controller_page_background_description": "பயன்பாட்டைத் திறக்கத் தேவையில்லாமல் எந்த புதிய சொத்துக்களையும் தானாக காப்புப் பிரதி எடுக்க பின்னணி சேவையை இயக்கவும்", + "backup_controller_page_background_is_off": "தானியங்கி பின்னணி காப்புப்பிரதி முடக்கப்பட்டுள்ளது", + "backup_controller_page_background_is_on": "தானியங்கி பின்னணி காப்புப்பிரதி இயக்கத்தில் உள்ளது", + "backup_controller_page_background_turn_off": "பின்னணி சேவையை அணைக்கவும்", + "backup_controller_page_background_turn_on": "பின்னணி சேவையை இயக்கவும்", + "backup_controller_page_background_wifi": "வைஃபை மட்டுமே", + "backup_controller_page_backup": "காப்புப்பிரதி", + "backup_controller_page_backup_selected": "தேர்ந்தெடுக்கப்பட்டது: ", + "backup_controller_page_backup_sub": "புகைப்படங்கள் மற்றும் வீடியோக்களை காப்புப் பிரதி எடுக்கவும்", + "backup_controller_page_created": "உருவாக்கப்பட்டது: {date}", + "backup_controller_page_desc_backup": "பயன்பாட்டைத் திறக்கும்போது புதிய சொத்துக்களை சேவையகத்தில் தானாக பதிவேற்ற முன்புற காப்புப்பிரதியை இயக்கவும்.", + "backup_controller_page_excluded": "விலக்கப்பட்டது: ", + "backup_controller_page_failed": "தோல்வியுற்றது ({count})", + "backup_controller_page_filename": "கோப்பு பெயர்: {filename} [{size}]", + "backup_controller_page_id": "ஐடி: {id}", + "backup_controller_page_info": "காப்புப்பிரதி செய்தி", + "backup_controller_page_none_selected": "எதுவும் தேர்ந்தெடுக்கப்படவில்லை", + "backup_controller_page_remainder": "மீதமுள்ள", + "backup_controller_page_remainder_sub": "தேர்விலிருந்து காப்புப் பிரதி எடுக்க மீதமுள்ள புகைப்படங்கள் மற்றும் வீடியோக்கள்", + "backup_controller_page_server_storage": "சேவையக சேமிப்பு", + "backup_controller_page_start_backup": "காப்புப்பிரதியைத் தொடங்கவும்", + "backup_controller_page_status_off": "தானியங்கி முன்புற காப்புப்பிரதி முடக்கப்பட்டுள்ளது", + "backup_controller_page_status_on": "தானியங்கி முன்புற காப்புப்பிரதி இயக்கத்தில் உள்ளது", + "backup_controller_page_storage_format": "{total} இல் {used} பயன்படுத்தப்பட்டது", + "backup_controller_page_to_backup": "ஆதரிக்கப்பட வேண்டிய ஆல்பங்கள்", + "backup_controller_page_total_sub": "தேர்ந்தெடுக்கப்பட்ட ஆல்பங்களிலிருந்து அனைத்து தனித்துவமான புகைப்படங்கள் மற்றும் வீடியோக்கள்", + "backup_controller_page_turn_off": "முன்புற காப்புப்பிரதியை அணைக்கவும்", + "backup_controller_page_turn_on": "முன்புற காப்புப்பிரதியை இயக்கவும்", + "backup_controller_page_uploading_file_info": "கோப்பு தகவலைப் பதிவேற்றுகிறது", + "backup_err_only_album": "ஒரே ஆல்பத்தை அகற்ற முடியாது", + "backup_info_card_assets": "சொத்துக்கள்", + "backup_manual_cancelled": "ரத்து செய்யப்பட்டது", + "backup_manual_in_progress": "ஏற்கனவே முன்னேற்றத்தில் பதிவேற்றவும். சிறிது நேரம் கழித்து முயற்சிக்கவும்", + "backup_manual_success": "செய்", + "backup_manual_title": "நிலை பதிவேற்றும் நிலை", + "backup_options": "காப்பு விருப்பங்கள்", + "backup_options_page_title": "காப்பு விருப்பங்கள்", + "backup_setting_subtitle": "பின்னணி மற்றும் முன்புற பதிவேற்ற அமைப்புகளை நிர்வகிக்கவும்", + "backup_settings_subtitle": "பதிவேற்ற அமைப்புகளை நிர்வகிக்கவும்", "backward": "பின்னோக்கு", + "biometric_auth_enabled": "பயோமெட்ரிக் ஏற்பு இயக்கப்பட்டது", + "biometric_locked_out": "நீங்கள் பயோமெட்ரிக் அங்கீகாரத்திலிருந்து பூட்டப்பட்டிருக்கிறீர்கள்", + "biometric_no_options": "பயோமெட்ரிக் விருப்பங்கள் எதுவும் கிடைக்கவில்லை", + "biometric_not_available": "இந்த சாதனத்தில் பயோமெட்ரிக் ஏற்பு கிடைக்கவில்லை", "birthdate_saved": "பிறந்த தேதி வெற்றிகரமாக சேமிக்கப்பட்டது", "birthdate_set_description": "ஒரு புகைப்படத்தின் போது இந்த நபரின் வயதைக் கணக்கிட பிறந்த தேதி பயன்படுத்தப்படுகிறது.", "blurred_background": "மங்கலான பின்னணி", @@ -420,31 +622,71 @@ "bulk_keep_duplicates_confirmation": "நீங்கள் {எண்ணிக்கை, பன்மை, ஒன்று {# நகல் சொத்து} பிற {# நகல் சொத்துக்கள்} be வைக்க விரும்புகிறீர்களா? இது எதையும் நீக்காமல் அனைத்து நகல் குழுக்களையும் தீர்க்கும்.", "bulk_trash_duplicates_confirmation": "நீங்கள் மொத்தமாக குப்பை {எண்ணிக்கை, பன்மை, ஒன்று {# நகல் சொத்து} பிற {# நகல் சொத்துக்கள்}}}} செய்ய விரும்புகிறீர்களா? இது ஒவ்வொரு குழுவின் மிகப்பெரிய சொத்தை வைத்திருக்கும் மற்றும் மற்ற அனைத்து நகல்களையும் குப்பைத் தொட்டியாக இருக்கும்.", "buy": "இம்மியை வாங்கவும்", + "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": "நகல் சொத்துக்கள் ({count})", + "cache_settings_statistics_album": "நூலக சிறு உருவங்கள்", + "cache_settings_statistics_full": "முழு படங்கள்", + "cache_settings_statistics_shared": "பகிரப்பட்ட ஆல்பம் சிறுபடம்", + "cache_settings_statistics_thumbnail": "சிறு உருவங்கள்", + "cache_settings_statistics_title": "கேச் பயன்பாடு", + "cache_settings_subtitle": "இம்மிச் மொபைல் பயன்பாட்டின் தற்காலிக சேமிப்பு நடத்தையை கட்டுப்படுத்தவும்", + "cache_settings_tile_subtitle": "உள்ளக சேமிப்பக நடத்தையை கட்டுப்படுத்தவும்", + "cache_settings_tile_title": "உள்ளக சேமிப்பு", + "cache_settings_title": "தேக்கக அமைப்புகள்", "camera": "கேமரா", "camera_brand": "கேமரா சூட்டுக்குறி", "camera_model": "கேமரா மாதிரி", "cancel": "ரத்துசெய்", "cancel_search": "தேடலை ரத்துசெய்", + "canceled": "ரத்து செய்யப்பட்டது", + "canceling": "ரத்துசெய்யும்", "cannot_merge_people": "மக்களை ஒன்றிணைக்க முடியாது", "cannot_undo_this_action": "இந்த செயலை நீங்கள் செயல்தவிர்க்க முடியாது!", "cannot_update_the_description": "விளக்கத்தை புதுப்பிக்க முடியாது", + "cast": "நடிகர்கள்", + "cast_description": "கிடைக்கக்கூடிய வார்ப்பு இடங்களை உள்ளமைக்கவும்", "change_date": "தேதியை மாற்றவும்", + "change_description": "விளக்கத்தை மாற்றவும்", + "change_display_order": "காட்சி வரிசையை மாற்றவும்", "change_expiration_time": "காலாவதி நேரத்தை மாற்றவும்", "change_location": "இருப்பிடத்தை மாற்றவும்", "change_name": "பெயரை மாற்றவும்", - "change_name_successfully": "பெயரை வெற்றிகரமாக மாற்றவும்", + "change_name_successfully": "பெயர் வெற்றிகரமாக மாற்றப்பட்டது", "change_password": "கடவுச்சொல்லை மாற்றவும்", "change_password_description": "நீங்கள் கணினியில் கையொப்பமிடுவது இதுவே முதல் முறை அல்லது உங்கள் கடவுச்சொல்லை மாற்றுவதற்கான கோரிக்கை செய்யப்பட்டுள்ளது. கீழே புதிய கடவுச்சொல்லை உள்ளிடவும்.", + "change_password_form_confirm_password": "கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "change_password_form_description": "ஆய் {name}, \n\nநீங்கள் கணினியில் கையொப்பமிடுவது இதுவே முதல் முறை அல்லது உங்கள் கடவுச்சொல்லை மாற்றுவதற்கான கோரிக்கை செய்யப்பட்டுள்ளது. கீழே புதிய கடவுச்சொல்லை உள்ளிடவும்.", + "change_password_form_new_password": "புதிய கடவுச்சொல்", + "change_password_form_password_mismatch": "கடவுச்சொற்கள் பொருந்தவில்லை", + "change_password_form_reenter_new_password": "புதிய கடவுச்சொல்லை மீண்டும் உள்ளிடவும்", + "change_pin_code": "முள் குறியீட்டை மாற்றவும்", "change_your_password": "உங்கள் கடவுச்சொல்லை மாற்றவும்", "changed_visibility_successfully": "தெரிவுநிலை வெற்றிகரமாக மாற்றப்பட்டது", + "charging": "சார்சிங்", + "charging_requirement_mobile_backup": "பின்னணி காப்புப்பிரதிக்கு சாதனம் சார்ச் செய்ய வேண்டும்", + "check_corrupt_asset_backup": "ஊழல் சொத்து காப்புப்பிரதிகளை சரிபார்க்கவும்", + "check_corrupt_asset_backup_button": "காசோலை செய்யுங்கள்", + "check_corrupt_asset_backup_description": "இந்த காசோலையை வைஃபை மீது மட்டுமே இயக்கவும், அனைத்து சொத்துக்களும் காப்புப் பிரதி எடுக்கப்பட்டவுடன். செயல்முறை சில நிமிடங்கள் ஆகலாம்.", "check_logs": "பதிவுகளை சரிபார்க்கவும்", "choose_matching_people_to_merge": "ஒன்றிணைக்க பொருந்தக்கூடிய நபர்களைத் தேர்வுசெய்க", "city": "நகரம்", "clear": "தெளிவான", "clear_all": "அனைத்தையும் அழிக்கவும்", "clear_all_recent_searches": "அண்மைக் கால அனைத்து தேடல்களையும் அழிக்கவும்", + "clear_file_cache": "கோப்பு தற்காலிக சேமிப்பை அழிக்கவும்", "clear_message": "தெளிவான செய்தி", "clear_value": "தெளிவான மதிப்பு", + "client_cert_dialog_msg_confirm": "சரி", + "client_cert_enter_password": "கடவுச்சொல்லை உள்ளிடவும்", + "client_cert_import": "இறக்குமதி", + "client_cert_import_success_msg": "கிளையன்ட் சான்றிதழ் இறக்குமதி செய்யப்படுகிறது", + "client_cert_invalid_msg": "தவறான சான்றிதழ் கோப்பு அல்லது தவறான கடவுச்சொல்", + "client_cert_remove_msg": "கிளையன்ட் சான்றிதழ் அகற்றப்பட்டது", + "client_cert_subtitle": "PKCS12 (.p12, .pfx) வடிவமைப்பை மட்டுமே ஆதரிக்கிறது. சான்றிதழ் இறக்குமதி/அகற்றுதல் உள்நுழைவதற்கு முன்புதான் கிடைக்கும்", + "client_cert_title": "எச்எச்எல் கிளையன்ட் சான்றிதழ்", "clockwise": "Locklowsy", "close": "மூடு", "collapse": "சரிவு", @@ -455,14 +697,31 @@ "comment_options": "கருத்து விருப்பங்கள்", "comments_and_likes": "கருத்துகள் மற்றும் விருப்பங்கள்", "comments_are_disabled": "கருத்துகள் முடக்கப்பட்டுள்ளன", + "common_create_new_album": "புதிய ஆல்பத்தை உருவாக்கவும்", + "common_server_error": "தயவுசெய்து உங்கள் பிணைய இணைப்பைச் சரிபார்க்கவும், சேவையகம் அடையக்கூடியது மற்றும் பயன்பாடு/சேவையக பதிப்புகள் இணக்கமானவை என்பதை உறுதிப்படுத்தவும்.", + "completed": "முடிந்தது", "confirm": "உறுதிப்படுத்தவும்", "confirm_admin_password": "நிர்வாகி கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "confirm_delete_face": "சொத்திலிருந்து {name} முகத்தை நீக்க விரும்புகிறீர்களா?", "confirm_delete_shared_link": "இந்த பகிரப்பட்ட இணைப்பை நீக்க விரும்புகிறீர்களா?", "confirm_keep_this_delete_others": "இந்த சொத்தைத் தவிர அடுக்கில் உள்ள மற்ற அனைத்து சொத்துகளும் நீக்கப்படும். நீங்கள் தொடர விரும்புகிறீர்களா?", + "confirm_new_pin_code": "புதிய முள் குறியீட்டை உறுதிப்படுத்தவும்", "confirm_password": "கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "confirm_tag_face": "இந்த முகத்தை {பெயர் அச் எனக் குறிக்க விரும்புகிறீர்களா?", + "confirm_tag_face_unnamed": "இந்த முகத்தை குறிக்க விரும்புகிறீர்களா?", + "connected_device": "இணைக்கப்பட்ட சாதனம்", + "connected_to": "இணைக்கப்பட்டுள்ளது", "contain": "கட்டுப்படுத்தவும்", "context": "சூழல்", "continue": "தொடரவும்", + "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": "இணைப்பைப் பகிரவும்", + "control_bottom_app_bar_share_to": "பகிர்ந்து கொள்ளுங்கள்", + "control_bottom_app_bar_trash_from_immich": "குப்பைக்கு நகர்த்தவும்", "copied_image_to_clipboard": "இடைநிலைப்பலகைக்கு படத்தை நகலெடுத்தது.", "copied_to_clipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது!", "copy_error": "நகல் பிழை", @@ -477,51 +736,91 @@ "covers": "மறையம்", "create": "உருவாக்கு", "create_album": "ஆல்பத்தை உருவாக்கவும்", + "create_album_page_untitled": "தலைப்பிடப்படாத", "create_library": "நூலகத்தை உருவாக்கவும்", "create_link": "இணைப்பை உருவாக்கவும்", "create_link_to_share": "பகிர்வுக்கு இணைப்பை உருவாக்கவும்", "create_link_to_share_description": "இணைப்பு உள்ள எவரும் தேர்ந்தெடுக்கப்பட்ட புகைப்படங்களைக் காணட்டும்)", + "create_new": "புதியதை உருவாக்கவும்", "create_new_person": "புதிய நபரை உருவாக்கவும்", "create_new_person_hint": "தேர்ந்தெடுக்கப்பட்ட சொத்துக்களை புதிய நபருக்கு ஒதுக்கவும்", "create_new_user": "புதிய பயனரை உருவாக்கவும்", + "create_shared_album_page_share_add_assets": "சொத்துக்களைச் சேர்க்கவும்", + "create_shared_album_page_share_select_photos": "புகைப்படங்களைத் தேர்ந்தெடுக்கவும்", + "create_shared_link": "பகிரப்பட்ட இணைப்பை உருவாக்கவும்", "create_tag": "குறிச்சொல்லை உருவாக்கவும்", "create_tag_description": "புதிய குறிச்சொல்லை உருவாக்கவும். உள்ளமைக்கப்பட்ட குறிச்சொற்களுக்கு, முன்னோக்கி ச்லாச்கள் உட்பட குறிச்சொல்லின் முழு பாதையையும் உள்ளிடவும்.", "create_user": "பயனரை உருவாக்கு", "created": "உருவாக்கப்பட்டது", + "created_at": "உருவாக்கப்பட்டது", + "creating_linked_albums": "இணைக்கப்பட்ட ஆல்பங்களை உருவாக்குதல் ...", + "crop": "பயிர்", + "curated_object_page_title": "விசயங்கள்", "current_device": "தற்போதைய சாதனம்", + "current_pin_code": "தற்போதைய முள் குறியீடு", + "current_server_address": "தற்போதைய சேவையக முகவரி", "custom_locale": "தனிப்பயன் இடம்", "custom_locale_description": "மொழி மற்றும் பிராந்தியத்தின் அடிப்படையில் வடிவமைப்பு தேதிகள் மற்றும் எண்கள்", + "custom_url": "தனிப்பயன் முகவரி", + "daily_title_text_date": "E, mmm dd", + "daily_title_text_date_year": "E, mmm dd, yyyy", "dark": "இருண்ட", + "dark_theme": "இருண்ட கருப்பொருளை மாற்றவும்", "date_after": "தேதி", "date_and_time": "தேதி மற்றும் நேரம்", "date_before": "முன் தேதி", + "date_format": "E, lll d, ஒய் • h: mm a", "date_of_birth_saved": "பிறந்த தேதி வெற்றிகரமாக சேமிக்கப்பட்டது", "date_range": "தேதி வரம்பு", "day": "நாள்", + "days": "நாட்கள்", "deduplicate_all": "அனைத்தையும் கழித்தல்", + "deduplication_criteria_1": "பைட்டுகளில் பட அளவு", + "deduplication_criteria_2": "EXIF தரவின் எண்ணிக்கை", + "deduplication_info": "கழித்தல் செய்தி", + "deduplication_info_description": "சொத்துக்களை தானாக முன்னெடுத்துச் செல்லவும், மொத்தமாக நகல்களை அகற்றவும், நாங்கள் பார்க்கிறோம்:", "default_locale": "இயல்புநிலை இடம்", "default_locale_description": "உங்கள் உலாவி இருப்பிடத்தின் அடிப்படையில் வடிவமைப்பு தேதிகள் மற்றும் எண்கள்", "delete": "நீக்கு", + "delete_action_confirmation_message": "இந்த சொத்தை நீக்க விரும்புகிறீர்களா? இந்த நடவடிக்கை சொத்தை சேவையகத்தின் குப்பைக்கு நகர்த்தும், மேலும் நீங்கள் அதை உள்நாட்டில் நீக்க விரும்பினால் கேட்கும்", + "delete_action_prompt": "{count} நீக்கப்பட்டது", "delete_album": "ஆல்பத்தை நீக்கு", "delete_api_key_prompt": "இந்த பநிஇ விசையை நீக்க விரும்புகிறீர்களா?", + "delete_dialog_alert": "இந்த உருப்படிகள் இம்மிச்சிலிருந்தும் உங்கள் சாதனத்திலிருந்தும் நிரந்தரமாக நீக்கப்படும்", + "delete_dialog_alert_local": "இந்த உருப்படிகள் உங்கள் சாதனத்திலிருந்து நிரந்தரமாக அகற்றப்படும், ஆனால் இன்னும் இம்மிச் சேவையகத்தில் கிடைக்கும்", + "delete_dialog_alert_local_non_backed_up": "சில உருப்படிகள் இம்மிச் வரை ஆதரிக்கப்படவில்லை, மேலும் அவை உங்கள் சாதனத்திலிருந்து நிரந்தரமாக அகற்றப்படும்", + "delete_dialog_alert_remote": "இந்த உருப்படிகள் இம்மிச் சேவையகத்திலிருந்து நிரந்தரமாக நீக்கப்படும்", + "delete_dialog_ok_force": "எப்படியும் நீக்கு", + "delete_dialog_title": "நிரந்தரமாக நீக்கு", "delete_duplicates_confirmation": "இந்த நகல்களை நிரந்தரமாக நீக்க விரும்புகிறீர்களா?", + "delete_face": "முகத்தை நீக்கு", "delete_key": "விசையை நீக்கு", "delete_library": "நூலகத்தை நீக்கு", "delete_link": "இணைப்பை நீக்கு", + "delete_local_action_prompt": "{count} உள்நாட்டில் நீக்கப்பட்டது", + "delete_local_dialog_ok_backed_up_only": "நீக்கு மட்டுமே காப்புப்பிரதி எடுக்கவும்", + "delete_local_dialog_ok_force": "எப்படியும் நீக்கு", "delete_others": "மற்றவர்களை நீக்கு", + "delete_permanently": "நிரந்தரமாக நீக்கு", + "delete_permanently_action_prompt": "{count} நிரந்தரமாக நீக்கப்பட்டது", "delete_shared_link": "பகிரப்பட்ட இணைப்பை நீக்கு", + "delete_shared_link_dialog_title": "பகிரப்பட்ட இணைப்பை நீக்கு", "delete_tag": "குறிச்சொல்லை நீக்கு", "delete_tag_confirmation_prompt": "{tagName} குறிச்சொல்லை நீக்க விரும்புகிறீர்களா?", "delete_user": "பயனரை நீக்கு", "deleted_shared_link": "பகிரப்பட்ட இணைப்பை நீக்கியது", "deletes_missing_assets": "வட்டில் இருந்து காணாமல் போன சொத்துக்களை நீக்குகிறது", "description": "விவரம்", + "description_input_hint_text": "விளக்கத்தைச் சேர்க்கவும் ...", + "description_input_submit_error": "விளக்கத்தை புதுப்பிப்பதில் பிழை, மேலும் விவரங்களுக்கு பதிவைச் சரிபார்க்கவும்", + "deselect_all": "அனைத்தையும் தேர்வு செய்யுங்கள்", "details": "விவரங்கள்", "direction": "திசை", "disabled": "முடக்கப்பட்டது", "disallow_edits": "திருத்தங்களை அனுமதிக்கவும்", "discord": "முரண்பாடு", "discover": "கண்டுபிடி", + "discovered_devices": "கண்டுபிடிக்கப்பட்ட சாதனங்கள்", "dismiss_all_errors": "அனைத்து பிழைகளையும் நிராகரிக்கவும்", "dismiss_error": "பிழையை நிராகரிக்கவும்", "display_options": "காட்சி விருப்பங்கள்", @@ -532,12 +831,26 @@ "documentation": "ஆவணப்படுத்துதல்", "done": "முடிந்தது", "download": "பதிவிறக்கம்", + "download_action_prompt": "பதிவிறக்கம் {count} சொத்துக்கள்", + "download_canceled": "பதிவிறக்கம் ரத்து செய்யப்பட்டது", + "download_complete": "முழுமையான பதிவிறக்கம்", + "download_enqueue": "Enqueuted பதிவிறக்க", + "download_error": "பிழையைப் பதிவிறக்கவும்", + "download_failed": "பதிவிறக்கம் தோல்வியடைந்தது", + "download_finished": "பதிவிறக்கம் முடிந்தது", "download_include_embedded_motion_videos": "உட்பொதிக்கப்பட்ட வீடியோக்கள்", "download_include_embedded_motion_videos_description": "மோசன் புகைப்படங்களில் உட்பொதிக்கப்பட்ட வீடியோக்களை தனி கோப்பாக சேர்க்கவும்", + "download_notfound": "பதிவிறக்கம் கிடைக்கவில்லை", + "download_paused": "இடைநிறுத்தப்பட்டது", "download_settings": "பதிவிறக்கம்", "download_settings_description": "சொத்து பதிவிறக்கம் தொடர்பான அமைப்புகளை நிர்வகிக்கவும்", + "download_started": "பதிவிறக்கம் தொடங்கியது", + "download_sucess": "வெற்றியைப் பதிவிறக்கவும்", + "download_sucess_android": "ஊடகங்கள் DCIM/IMMich க்கு பதிவிறக்கம் செய்யப்பட்டுள்ளன", + "download_waiting_to_retry": "மீண்டும் முயற்சிக்க காத்திருக்கிறது", "downloading": "பதிவிறக்குகிறது", "downloading_asset_filename": "சொத்து பதிவிறக்கம் {filename}", + "downloading_media": "ஊடகங்களைப் பதிவிறக்குகிறது", "drop_files_to_upload": "பதிவேற்ற எங்கும் கோப்புகளை விடுங்கள்", "duplicates": "நகல்கள்", "duplicates_description": "ஒவ்வொரு குழுவையும் எந்த நகல்களிலும் குறிப்பிடுவதன் மூலம் தீர்க்கவும்", @@ -545,8 +858,14 @@ "edit": "தொகு", "edit_album": "ஆல்பத்தைத் திருத்து", "edit_avatar": "அவதாரத்தைத் திருத்து", + "edit_birthday": "பிறந்தநாளைத் திருத்தவும்", "edit_date": "தேதியைத் திருத்து", "edit_date_and_time": "தேதி மற்றும் நேரத்தைத் திருத்தவும்", + "edit_date_and_time_action_prompt": "{count} தேதி மற்றும் நேரம் திருத்தப்பட்டது", + "edit_date_and_time_by_offset": "ஆஃப்செட் மூலம் தேதியை மாற்றவும்", + "edit_date_and_time_by_offset_interval": "புதிய தேதி வரம்பு: {from} - {to}", + "edit_description": "விளக்கத்தைத் திருத்து", + "edit_description_prompt": "புதிய விளக்கத்தைத் தேர்ந்தெடுக்கவும்:", "edit_exclusion_pattern": "விலக்கு முறையைத் திருத்தவும்", "edit_faces": "முகங்களைத் திருத்தவும்", "edit_import_path": "இறக்குமதி பாதையைத் திருத்து", @@ -554,6 +873,8 @@ "edit_key": "திறனைத் திருத்து", "edit_link": "இணைப்பைத் திருத்து", "edit_location": "இருப்பிடத்தைத் திருத்தவும்", + "edit_location_action_prompt": "{count} இருப்பிடம் திருத்தப்பட்டது", + "edit_location_dialog_title": "இடம்", "edit_name": "பெயரைத் திருத்து", "edit_people": "மக்களைத் திருத்தவும்", "edit_tag": "குறிச்சொல்லைத் திருத்து", @@ -566,13 +887,27 @@ "editor_crop_tool_h2_aspect_ratios": "அம்ச விகிதங்கள்", "editor_crop_tool_h2_rotation": "சுழற்சி", "email": "மின்னஞ்சல்", + "email_notifications": "மின்னஞ்சல் அறிவிப்புகள்", + "empty_folder": "இந்த கோப்புறை காலியாக உள்ளது", "empty_trash": "வெற்று குப்பை", "empty_trash_confirmation": "நீங்கள் குப்பைகளை வெறுமை செய்ய விரும்புகிறீர்களா? இது குப்பையில் உள்ள அனைத்து சொத்துக்களையும் நிரந்தரமாக இம்மிச்சிலிருந்து அகற்றும்.\n இந்த செயலை நீங்கள் செயல்தவிர்க்க முடியாது!", "enable": "இயக்கு", + "enable_backup": "காப்புப்பிரதியை இயக்கவும்", + "enable_biometric_auth_description": "பயோமெட்ரிக் அங்கீகாரத்தை இயக்க உங்கள் முள் குறியீட்டை உள்ளிடவும்", "enabled": "இயக்கப்பட்டது", "end_date": "இறுதி தேதி", + "enqueued": "Enqueuted", + "enter_wifi_name": "வைஃபை பெயரை உள்ளிடவும்", + "enter_your_pin_code": "உங்கள் முள் குறியீட்டை உள்ளிடவும்", + "enter_your_pin_code_subtitle": "பூட்டப்பட்ட கோப்புறையை அணுக உங்கள் முள் குறியீட்டை உள்ளிடவும்", "error": "பிழை", + "error_change_sort_album": "ஆல்பம் வரிசை வரிசையை மாற்றத் தவறிவிட்டது", + "error_delete_face": "சொத்தில் இருந்து முகத்தை நீக்குவதில் பிழை", + "error_getting_places": "இடங்களைப் பெறுவதில் பிழை", "error_loading_image": "படத்தை ஏற்றுவதில் பிழை", + "error_loading_partners": "கூட்டாளர்களை ஏற்றுவதில் பிழை: {error}", + "error_saving_image": "பிழை: {error}", + "error_tag_face_bounding_box": "முகத்தை குறிக்கவும் பிழை - எல்லை பெட்டி ஆயத்தொலைவுகளைப் பெற முடியாது", "error_title": "பிழை - ஏதோ தவறு நடந்தது", "errors": { "cannot_navigate_next_asset": "அடுத்த சொத்துக்கு செல்ல முடியாது", @@ -600,15 +935,19 @@ "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_reset_pin_code": "முள் குறியீட்டை மீட்டமைக்கத் தவறிவிட்டது", "failed_to_stack_assets": "சொத்துக்களை அடுக்கி வைப்பதில் தோல்வி", "failed_to_unstack_assets": "அன்-ச்டாக் சொத்துக்களில் தோல்வியுற்றது", + "failed_to_update_notification_status": "அறிவிப்பு நிலையைப் புதுப்பிக்கத் தவறிவிட்டது", "import_path_already_exists": "இந்த இறக்குமதி பாதை ஏற்கனவே உள்ளது.", "incorrect_email_or_password": "தவறான மின்னஞ்சல் அல்லது கடவுச்சொல்", "paths_validation_failed": "{பாதைகள், பன்மை, ஒன்று {# பாதை} மற்ற {# பாதைகள்}} தோல்வியுற்ற சரிபார்ப்பு", "profile_picture_transparent_pixels": "சுயவிவரப் படங்களுக்கு வெளிப்படையான படப்புள்ளிகள் இருக்க முடியாது. தயவுசெய்து பெரிதாக்கவும்/அல்லது படத்தை நகர்த்தவும்.", "quota_higher_than_disk_size": "வட்டு அளவை விட அதிகமாக ஒதுக்கீட்டை அமைத்துள்ளீர்கள்", + "something_went_wrong": "ஏதோ தவறு நடந்தது", "unable_to_add_album_users": "ஆல்பத்தில் பயனர்களைச் சேர்க்க முடியவில்லை", "unable_to_add_assets_to_shared_link": "பகிரப்பட்ட இணைப்புக்கு சொத்துக்களைச் சேர்க்க முடியவில்லை", "unable_to_add_comment": "கருத்து சேர்க்க முடியவில்லை", @@ -620,6 +959,7 @@ "unable_to_archive_unarchive": "{காப்பகப்படுத்த முடியவில்லை, தேர்ந்தெடுக்க முடியவில்லை, உண்மை {archive} பிற {unarchive}}", "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": "கடவுச்சொல்லை மாற்ற முடியவில்லை", @@ -663,6 +1003,7 @@ "unable_to_remove_partner": "கூட்டாளரை அகற்ற முடியவில்லை", "unable_to_remove_reaction": "எதிர்வினையை அகற்ற முடியவில்லை", "unable_to_reset_password": "கடவுச்சொல்லை மீட்டமைக்க முடியவில்லை", + "unable_to_reset_pin_code": "முள் குறியீட்டை மீட்டமைக்க முடியவில்லை", "unable_to_resolve_duplicate": "நகல் தீர்க்க முடியவில்லை", "unable_to_restore_assets": "சொத்துக்களை மீட்டெடுக்க முடியவில்லை", "unable_to_restore_trash": "குப்பைகளை மீட்டெடுக்க முடியவில்லை", @@ -690,8 +1031,19 @@ "unable_to_update_user": "பயனரைப் புதுப்பிக்க முடியவில்லை", "unable_to_upload_file": "கோப்பைப் பதிவேற்ற முடியவில்லை" }, + "exif": "Exif", + "exif_bottom_sheet_description": "விளக்கத்தைச் சேர்க்கவும் ...", + "exif_bottom_sheet_description_error": "விளக்கத்தை புதுப்பிப்பதில் பிழை", + "exif_bottom_sheet_details": "விவரங்கள்", + "exif_bottom_sheet_location": "இடம்", + "exif_bottom_sheet_people": "மக்கள்", + "exif_bottom_sheet_person_add_person": "பெயரைச் சேர்க்கவும்", "exit_slideshow": "ச்லைடுசோவிலிருந்து வெளியேறவும்", "expand_all": "அனைத்தையும் விரிவாக்குங்கள்", + "experimental_settings_new_asset_list_subtitle": "வேலை முன்னேற்றத்தில் உள்ளது", + "experimental_settings_new_asset_list_title": "சோதனை புகைப்பட கட்டத்தை இயக்கவும்", + "experimental_settings_subtitle": "உங்கள் சொந்த ஆபத்தில் பயன்படுத்தவும்!", + "experimental_settings_title": "சோதனை", "expire_after": "பின்னர் காலாவதியாகுங்கள்", "expired": "காலாவதியான", "expires_date": "{date} காலாவதியாகிறது", @@ -699,37 +1051,74 @@ "explorer": "எக்ச்ப்ளோரர்", "export": "ஏற்றுமதி", "export_as_json": "சாதொபொகு ஆக ஏற்றுமதி", + "export_database": "ஏற்றுமதி தரவுத்தளம்", + "export_database_description": "SQLITE தரவுத்தளத்தை ஏற்றுமதி செய்யுங்கள்", "extension": "நீட்டிப்பு", "external": "வெளிப்புறம்", "external_libraries": "வெளிப்புற நூலகங்கள்", + "external_network": "வெளிப்புற பிணையம்", + "external_network_sheet_info": "விருப்பமான வைஃபை நெட்வொர்க்கில் இல்லாதபோது, பயன்பாடு சேவையகத்துடன் கீழே உள்ள முகவரி களின் முதல் வழியாக இணைக்கப்படும், இது மேலே இருந்து கீழே தொடங்குகிறது", "face_unassigned": "ஒதுக்கப்படாதது", + "failed": "தோல்வியுற்றது", + "failed_to_authenticate": "அங்கீகரிக்கத் தவறிவிட்டது", "failed_to_load_assets": "சொத்துக்களை ஏற்றுவதில் தோல்வி", + "failed_to_load_folder": "கோப்புறையை ஏற்றுவதில் தோல்வி", "favorite": "பிடித்த", + "favorite_action_prompt": "{எண்ணிக்கை the பிடித்தவைகளில் சேர்க்கப்பட்டது", "favorite_or_unfavorite_photo": "பிடித்த அல்லது சாதகமற்ற புகைப்படம்", "favorites": "பிடித்தவை", + "favorites_page_no_favorites": "பிடித்த சொத்துக்கள் எதுவும் கிடைக்கவில்லை", "feature_photo_updated": "அம்ச புகைப்படம் புதுப்பிக்கப்பட்டது", "features": "நற்பொருத்தங்கள்", + "features_in_development": "வளர்ச்சியில் நற்பொருத்தங்கள்", "features_setting_description": "பயன்பாட்டு அம்சங்களை நிர்வகிக்கவும்", "file_name": "கோப்பு பெயர்", "file_name_or_extension": "கோப்பு பெயர் அல்லது நீட்டிப்பு", "filename": "கோப்புப்பெயர்", "filetype": "பைல்டைப்", + "filter": "வடிப்பி", "filter_people": "மக்களை வடிகட்டவும்", + "filter_places": "இடங்களை வடிகட்டவும்", "find_them_fast": "தேடலுடன் பெயரால் வேகமாக அவற்றைக் கண்டறியவும்", + "first": "முதல்", "fix_incorrect_match": "தவறான போட்டியை சரிசெய்யவும்", + "folder": "கோப்புறை", + "folder_not_found": "கோப்புறை கிடைக்கவில்லை", "folders": "கோப்புறைகள்", "folders_feature_description": "கோப்பு முறைமையில் உள்ள புகைப்படங்கள் மற்றும் வீடியோக்களுக்கான கோப்புறை காட்சியை உலாவுதல்", + "forgot_pin_code_question": "உங்கள் முள் மறந்துவிட்டீர்களா?", "forward": "முன்னோக்கி", + "gcast_enabled": "கூகிள் நடிகர்கள்", + "gcast_enabled_description": "இந்த நற்பொருத்தம் வேலை செய்வதற்காக Google இலிருந்து வெளிப்புற வளங்களை ஏற்றுகிறது.", "general": "பொது", + "geolocation_instruction_location": "அதன் இருப்பிடத்தைப் பயன்படுத்த சி.பி.எச் ஆயத்தொலைவுகளுடன் ஒரு சொத்தில் சொடுக்கு செய்க அல்லது வரைபடத்திலிருந்து நேரடியாக ஒரு இடத்தைத் தேர்ந்தெடுக்கவும்", "get_help": "உதவி பெறு", + "get_wifiname_error": "வைஃபை பெயரைப் பெற முடியவில்லை. நீங்கள் தேவையான அனுமதிகளை வழங்கியுள்ளீர்கள் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள் மற்றும் வைஃபை நெட்வொர்க்குடன் இணைக்கப்பட்டுள்ளீர்கள்", "getting_started": "தொடங்குதல்", "go_back": "திரும்பிச் செல்லுங்கள்", + "go_to_folder": "கோப்புறைக்குச் செல்லுங்கள்", "go_to_search": "தேடச் செல்லவும்", + "gps": "உலக இடம் காட்டும் அமைப்பு (இடம் காட்டி)", + "gps_missing": "சி.பி.எச் இல்லை", + "grant_permission": "இசைவு வழங்கவும்", "group_albums_by": "குழு ஆல்பங்கள் வழங்கியவர் ...", + "group_country": "நாடு மூலம் குழு", "group_no": "குழு இல்லை", "group_owner": "உரிமையாளரால் குழு", + "group_places_by": "குழு இடங்கள் ...", "group_year": "ஆண்டுக்கு குழு", + "haptic_feedback_switch": "ஆப்டிக் பின்னூட்டத்தை இயக்கவும்", + "haptic_feedback_title": "ஆப்டிக் கருத்து", "has_quota": "ஒதுக்கீடு உள்ளது", + "hash_asset": "ஆச் சொத்து", + "hashed_assets": "சொத்துக்கள்", + "hashing": "ஏசிங்", + "header_settings_add_header_tip": "தலைப்புச் சேர்க்கவும்", + "header_settings_field_validator_msg": "மதிப்பு காலியாக இருக்க முடியாது", + "header_settings_header_name_input": "தலைப்பு பெயர்", + "header_settings_header_value_input": "தலைப்பு மதிப்பு", + "headers_settings_tile_subtitle": "ஒவ்வொரு பிணைய கோரிக்கையுடனும் பயன்பாடு அனுப்ப வேண்டிய பதிலாள் தலைப்புகளை வரையறுக்கவும்", + "headers_settings_tile_title": "தனிப்பயன் பதிலாள் தலைப்புகள்", "hi_user": "ஆய் {name} ({email})", "hide_all_people": "எல்லா மக்களையும் மறைக்கவும்", "hide_gallery": "கேலரியை மறைக்கவும்", @@ -737,8 +1126,29 @@ "hide_password": "கடவுச்சொல்லை மறைக்கவும்", "hide_person": "நபரை மறைக்க", "hide_unnamed_people": "பெயரிடப்படாதவர்களை மறைக்கவும்", + "home_page_add_to_album_conflicts": "சேர்க்கப்பட்டது {ஆல்பம் {added} சொத்துக்கள் {ஆல்பம். {album} சொத்துக்கள் ஏற்கனவே ஆல்பத்தில் உள்ளன.", + "home_page_add_to_album_err_local": "ஆல்பங்களில் உள்ளக சொத்துக்களை இன்னும் சேர்க்க முடியாது, தவிர்க்கவும்", + "home_page_add_to_album_success": "சேர்க்கப்பட்டது {ஆல்பம் {added} சொத்துக்கள் {ஆல்பம்.", + "home_page_album_err_partner": "ஒரு ஆல்பத்திற்கு இன்னும் கூட்டாளர் சொத்துக்களைச் சேர்க்க முடியவில்லை, தவிர்க்கவும்", + "home_page_archive_err_local": "உள்ளக சொத்துக்களை இன்னும் காப்பகப்படுத்த முடியாது, தவிர்க்கவும்", + "home_page_archive_err_partner": "கூட்டாளர் சொத்துக்களை காப்பகப்படுத்த முடியாது, தவிர்க்கவும்", + "home_page_building_timeline": "காலவரிசையை உருவாக்குதல்", + "home_page_delete_err_partner": "கூட்டாளர் சொத்துக்களை நீக்க முடியாது, தவிர்க்கவும்", + "home_page_delete_remote_err_local": "தொலைநிலை தேர்வை நீக்கு, தவிர்ப்பதில் உள்ளக சொத்துக்கள்", + "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": "மணி", + "hours": "மணி", + "id": "ஐடி", + "idle": "நிலையிக்கம்", + "ignore_icloud_photos": "ICloud புகைப்படங்களை புறக்கணிக்கவும்", + "ignore_icloud_photos_description": "ICloud இல் சேமிக்கப்படும் புகைப்படங்கள் இம்மிச் சேவையகத்தில் பதிவேற்றப்படாது", "image": "படம்", "image_alt_text_date": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {date} இல் எடுக்கப்பட்டது", "image_alt_text_date_1_person": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {{person1} இல் {date}", @@ -749,7 +1159,11 @@ "image_alt_text_date_place_1_person": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {city}, {country} {person1} உடன் {date}", "image_alt_text_date_place_2_people": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {city}, {country} உடன் {person1} மற்றும் {person2} {date}", "image_alt_text_date_place_3_people": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {city}, {country} {person1}, {person2}, மற்றும் {person3} இல் எடுக்கப்பட்டது {date}", - "image_alt_text_date_place_4_or_more_people": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {city}, {country} {person1}, {person2}, மற்றும் {கூடுதல் கவுன்ட், எண்} மற்றவர்கள் {date}", + "image_alt_text_date_place_4_or_more_people": "{isvideo, தேர்ந்தெடு, உண்மை {Video} பிற {Image}} {city}, {country} {person1}, {person2}, மற்றும் {கூடுதல் அக்கவுண்ட், எண்} மற்றவர்கள் {date}", + "image_saved_successfully": "படம் சேமிக்கப்பட்டது", + "image_viewer_page_state_provider_download_started": "பதிவிறக்கம் தொடங்கியது", + "image_viewer_page_state_provider_download_success": "வெற்றியைப் பதிவிறக்கவும்", + "image_viewer_page_state_provider_share_error": "பிழை பகிர்வு", "immich_logo": "இம்மிச் லோகோ", "immich_web_interface": "இம்ரிச் வலை இடைமுகம்", "import_from_json": "சாதொபொகு இலிருந்து இறக்குமதி", @@ -760,6 +1174,7 @@ "include_shared_albums": "பகிரப்பட்ட ஆல்பங்களைச் சேர்க்கவும்", "include_shared_partner_assets": "பகிரப்பட்ட கூட்டாளர் சொத்துக்களைச் சேர்க்கவும்", "individual_share": "தனிப்பட்ட பங்கு", + "individual_shares": "தனிப்பட்ட பங்குகள்", "info": "தகவல்", "interval": { "day_at_onepm": "ஒவ்வொரு நாளும் மதியம் 1 மணிக்கு", @@ -767,8 +1182,16 @@ "night_at_midnight": "ஒவ்வொரு இரவும் நள்ளிரவில்", "night_at_twoam": "ஒவ்வொரு இரவும் அதிகாலை 2 மணிக்கு" }, + "invalid_date": "தவறான தேதி", + "invalid_date_format": "தவறான தேதி வடிவம்", "invite_people": "மக்களை அழைக்கவும்", "invite_to_album": "ஆல்பத்திற்கு அழைக்கவும்", + "ios_debug_info_fetch_ran_at": "ஓடியது {dateTime}", + "ios_debug_info_last_sync_at": "கடைசி ஒத்திசைவு {dateTime}", + "ios_debug_info_no_processes_queued": "பின்னணி செயல்முறைகள் வரிசையில் இல்லை", + "ios_debug_info_no_sync_yet": "பின்னணி ஒத்திசைவு வேலை இன்னும் இயங்கவில்லை", + "ios_debug_info_processes_queued": "{எண்ணிக்கை, பன்மை, ஒன்று {{count} பின்னணி செயல்முறை வரிசையில் வரிசைப்படுத்தப்பட்டது} பிற {{count} பின்னணி செயல்முறைகள் வரிசைப்படுத்தப்பட்டன", + "ios_debug_info_processing_ran_at": "செயலாக்கம் {dateTime}", "items_count": "{எண்ணிக்கை, பன்மை, ஒன்று {# உருப்படி} பிற {# உருப்படிகள்}}", "jobs": "வேலைகள்", "keep": "வைத்திருங்கள்", @@ -777,16 +1200,31 @@ "kept_this_deleted_others": "இந்த சொத்தை வைத்து நீக்கப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்}}", "keyboard_shortcuts": "விசைப்பலகை குறுக்குவழிகள்", "language": "மொழி", + "language_no_results_subtitle": "உங்கள் தேடல் காலத்தை சரிசெய்ய முயற்சிக்கவும்", + "language_no_results_title": "எந்த மொழிகளும் கிடைக்கவில்லை", + "language_search_hint": "மொழிகளைத் தேடுங்கள் ...", "language_setting_description": "உங்களுக்கு விருப்பமான மொழியைத் தேர்ந்தெடுக்கவும்", + "large_files": "பெரிய கோப்புகள்", + "last": "கடைசி", "last_seen": "கடைசியாக பார்த்தேன்", "latest_version": "அண்மைக் கால பதிப்பு", "latitude": "அகலாங்கு", "leave": "விடுப்பு", + "leave_album": "விடுப்பு ஆல்பம்", + "lens_model": "லென்ச் மாதிரி", "let_others_respond": "மற்றவர்கள் பதிலளிக்கட்டும்", "level": "நிலை", "library": "நூலகம்", "library_options": "நூலக விருப்பங்கள்", + "library_page_device_albums": "சாதனத்தில் ஆல்பங்கள்", + "library_page_new_album": "புதிய ஆல்பம்", + "library_page_sort_asset_count": "சொத்துக்களின் எண்ணிக்கை", + "library_page_sort_created": "உருவாக்கப்பட்ட தேதி", + "library_page_sort_last_modified": "கடைசியாக மாற்றப்பட்டது", + "library_page_sort_title": "ஆல்பம் தலைப்பு", + "licenses": "உரிமங்கள்", "light": "ஒளி", + "like": "போன்ற", "like_deleted": "நீக்கப்பட்டதைப் போல", "link_motion_video": "இணைப்பு இயக்க வீடியோ", "link_to_oauth": "OAUTH உடன் இணைப்பு", @@ -794,20 +1232,61 @@ "list": "பட்டியல்", "loading": "ஏற்றுகிறது", "loading_search_results_failed": "தேடல் முடிவுகளை ஏற்றுவது தோல்வியடைந்தது", + "local": "உள்ளக", + "local_asset_cast_failed": "சேவையகத்தில் பதிவேற்றப்படாத ஒரு சொத்தை அனுப்ப முடியவில்லை", + "local_assets": "உள்ளக சொத்துக்கள்", + "local_media_summary": "உள்ளக ஊடக சுருக்கம்", + "local_network": "உள்ளக பிணையம்", + "local_network_sheet_info": "குறிப்பிட்ட வைஃபை நெட்வொர்க்கைப் பயன்படுத்தும் போது பயன்பாடு இந்த முகவரி மூலம் சேவையகத்துடன் இணைக்கப்படும்", + "location_permission": "இருப்பிட இசைவு", + "location_permission_content": "ஆட்டோ-ச்விட்சிங் அம்சத்தைப் பயன்படுத்த, இம்மிக்கு துல்லியமான இருப்பிட இசைவு தேவை, எனவே இது தற்போதைய வைஃபை நெட்வொர்க்கின் பெயரைப் படிக்க முடியும்", + "location_picker_choose_on_map": "வரைபடத்தில் தேர்வு செய்யவும்", + "location_picker_latitude_error": "செல்லுபடியாகும் அட்சரேகை உள்ளிடவும்", + "location_picker_latitude_hint": "உங்கள் அட்சரேகையை இங்கே உள்ளிடவும்", + "location_picker_longitude_error": "செல்லுபடியாகும் தீர்க்கரேகையை உள்ளிடவும்", + "location_picker_longitude_hint": "உங்கள் தீர்க்கரேகையை இங்கே உள்ளிடவும்", + "lock": "பூட்டு", + "locked_folder": "பூட்டப்பட்ட கோப்புறை", + "log_detail_title": "பதிவு விவரம்", "log_out": "விடுபதிகை", "log_out_all_devices": "எல்லா சாதனங்களையும் விட்டு வெளியேறவும்", + "logged_in_as": "{user}", "logged_out_all_devices": "எல்லா சாதனங்களையும் வெளியேற்றினேன்", "logged_out_device": "உள்நுழைந்த சாதனம்", "login": "புகுபதிவு", + "login_disabled": "உள்நுழைவு முடக்கப்பட்டுள்ளது", + "login_form_api_exception": "பநிஇ விதிவிலக்கு. சேவையக முகவரி ஐ சரிபார்த்து மீண்டும் முயற்சிக்கவும்.", + "login_form_back_button_text": "பின்", + "login_form_email_hint": "youremail@email.com", + "login_form_endpoint_hint": "http: // your-server-ip: துறைமுகம்", + "login_form_endpoint_url": "சேவையக எண்ட்பாயிண்ட் முகவரி", + "login_form_err_http": "Http: // அல்லது https: // ஐக் குறிப்பிடவும்", + "login_form_err_invalid_email": "தவறான மின்னஞ்சல்", + "login_form_err_invalid_url": "தவறான முகவரி", + "login_form_err_leading_whitespace": "முன்னணி விட்ச்பேச்", + "login_form_err_trailing_whitespace": "பின்திங் வைட்ச்பேச்", + "login_form_failed_get_oauth_server_config": "OAuth ஐப் பயன்படுத்தி பதிவுசெய்தல், சேவையக முகவரி ஐ சரிபார்க்கவும்", + "login_form_failed_get_oauth_server_disable": "இந்த சேவையகத்தில் OAuth நற்பொருத்தம் கிடைக்கவில்லை", + "login_form_failed_login": "உங்களை உள்நுழைவதில் பிழை, சேவையக URL, மின்னஞ்சல் மற்றும் கடவுச்சொல்லை சரிபார்க்கவும்", + "login_form_handshake_exception": "சேவையகத்துடன் ஏண்ட்சேக் விதிவிலக்கு இருந்தது. நீங்கள் தன்வய கையொப்பமிடப்பட்ட சான்றிதழைப் பயன்படுத்துகிறீர்கள் என்றால் அமைப்புகளில் தன்வய கையொப்பமிடப்பட்ட சான்றிதழ் ஆதரவை இயக்கவும்.", + "login_form_password_hint": "கடவுச்சொல்", + "login_form_save_login": "உள்நுழைந்திருங்கள்", + "login_form_server_empty": "சேவையக முகவரி ஐ உள்ளிடவும்.", + "login_form_server_error": "சேவையகத்துடன் இணைக்க முடியவில்லை.", "login_has_been_disabled": "உள்நுழைவு முடக்கப்பட்டுள்ளது.", + "login_password_changed_error": "உங்கள் கடவுச்சொல்லைப் புதுப்பிப்பதில் பிழை ஏற்பட்டது", + "login_password_changed_success": "கடவுச்சொல் வெற்றிகரமாக புதுப்பிக்கப்பட்டது", "logout_all_device_confirmation": "எல்லா சாதனங்களையும் விட்டு வெளியேற விரும்புகிறீர்களா?", "logout_this_device_confirmation": "இந்த சாதனத்தை விட்டு வெளியேற விரும்புகிறீர்களா?", + "logs": "பதிவுகள்", "longitude": "நெட்டாங்கு", "look": "பார்", "loop_videos": "லூப் வீடியோக்கள்", "loop_videos_description": "விரிவான பார்வையாளரில் ஒரு வீடியோவை தானாக வளையப்படுத்தவும்.", - "main_branch_warning": "நீங்கள் மேம்பாட்டு பதிப்பைப் பயன்படுத்துகிறீர்கள்; வெளியீட்டு பதிப்பைப் பயன்படுத்த நாங்கள் கடுமையாக பரிந்துரைக்கிறோம்!", + "main_branch_warning": "நீங்கள் ஒரு மேம்பாட்டு பதிப்பைப் பயன்படுத்துகிறீர்கள்; வெளியீட்டு பதிப்பைப் பயன்படுத்த நாங்கள் கடுமையாக பரிந்துரைக்கிறோம்!", + "main_menu": "பட்டியல் விளையாடுங்கள்", "make": "உருவாக்கு", + "manage_geolocation": "இருப்பிடத்தை நிர்வகிக்கவும்", "manage_shared_links": "பகிரப்பட்ட இணைப்புகளை நிர்வகிக்கவும்", "manage_sharing_with_partners": "கூட்டாளர்களுடன் பகிர்வை நிர்வகிக்கவும்", "manage_the_app_settings": "பயன்பாட்டு அமைப்புகளை நிர்வகிக்கவும்", @@ -816,13 +1295,40 @@ "manage_your_devices": "உங்கள் உள்நுழைந்த சாதனங்களை நிர்வகிக்கவும்", "manage_your_oauth_connection": "உங்கள் OAuth இணைப்பை நிர்வகிக்கவும்", "map": "வரைபடம்", + "map_assets_in_bounds": "{எண்ணிக்கை, பன்மை, = 0 {No photos in this area} ஒன்று {# புகைப்படம்} மற்ற {# புகைப்படங்கள்}}", + "map_cannot_get_user_location": "பயனரின் இருப்பிடத்தைப் பெற முடியாது", + "map_location_dialog_yes": "ஆம்", + "map_location_picker_page_use_location": "இந்த இருப்பிடத்தைப் பயன்படுத்தவும்", + "map_location_service_disabled_content": "உங்கள் தற்போதைய இருப்பிடத்திலிருந்து சொத்துக்களைக் காட்ட இருப்பிட பணி இயக்கப்பட வேண்டும். இப்போது அதை இயக்க விரும்புகிறீர்களா?", + "map_location_service_disabled_title": "இருப்பிட பணி முடக்கப்பட்டது", "map_marker_for_images": "{city}, {country}", "map_marker_with_image": "படத்துடன் வரைபட மார்க்கர்", + "map_no_location_permission_content": "உங்கள் தற்போதைய இருப்பிடத்திலிருந்து சொத்துக்களைக் காட்ட இருப்பிட இசைவு தேவை. இப்போது அதை அனுமதிக்க விரும்புகிறீர்களா?", + "map_no_location_permission_title": "இருப்பிட இசைவு மறுக்கப்பட்டது", "map_settings": "வரைபட அமைப்புகள்", + "map_settings_dark_mode": "இருண்ட முறை", + "map_settings_date_range_option_day": "கடந்த 24 மணி நேரம்", + "map_settings_date_range_option_days": "கடந்த {days} நாட்கள்", + "map_settings_date_range_option_year": "கடந்த ஆண்டு", + "map_settings_date_range_option_years": "கடந்த {years} ஆண்டுகள்", + "map_settings_dialog_title": "வரைபட அமைப்புகள்", + "map_settings_include_show_archived": "காப்பகப்படுத்தப்பட்டவர்", + "map_settings_include_show_partners": "கூட்டாளர்களைச் சேர்க்கவும்", + "map_settings_only_show_favorites": "பிடித்ததை மட்டும் காட்டு", + "map_settings_theme_settings": "வரைபட கருப்பொருள்", + "map_zoom_to_see_photos": "புகைப்படங்களைக் காண பெரிதாக்கவும்", + "mark_all_as_read": "அனைத்தையும் படித்தபடி குறிக்கவும்", + "mark_as_read": "படித்தபடி குறி", + "marked_all_as_read": "அனைத்தையும் வாசிப்பதாக குறிக்கப்பட்டுள்ளது", "matches": "போட்டிகள்", + "matching_assets": "பொருந்தும் சொத்துக்கள்", "media_type": "ஊடக வகை", "memories": "நினைவுகள்", + "memories_all_caught_up": "அனைவரும் பிடிபட்டனர்", + "memories_check_back_tomorrow": "மேலும் நினைவுகளுக்கு நாளை மீண்டும் பார்க்கவும்", "memories_setting_description": "உங்கள் நினைவுகளில் நீங்கள் பார்ப்பதை நிர்வகிக்கவும்", + "memories_start_over": "தொடக்க", + "memories_swipe_to_close": "மூடுவதற்கு ச்வைப் செய்யவும்", "memory": "நினைவகம்", "memory_lane_title": "நினைவக லேன் {title}", "menu": "பட்டியல்", @@ -834,19 +1340,40 @@ "merged_people_count": "ஒன்றிணைக்கப்பட்ட {எண்ணிக்கை, பன்மை, ஒன்று {# நபர்} மற்ற {# மக்கள்}}", "minimize": "குறைக்கவும்", "minute": "நிமிடங்கள்", + "minutes": "நிமிடங்கள்", "missing": "இல்லை", "model": "மாதிரியுரு", "month": "மாதம்", + "monthly_title_text_date_format": "Mmmm ஒய்", "more": "மேலும்", + "move": "நகர்த்தவும்", + "move_off_locked_folder": "பூட்டப்பட்ட கோப்புறையிலிருந்து வெளியேறவும்", + "move_to_lock_folder_action_prompt": "{எண்ணிக்கை the பூட்டப்பட்ட கோப்புறையில் சேர்க்கப்பட்டுள்ளது", + "move_to_locked_folder": "பூட்டப்பட்ட கோப்புறையில் செல்லுங்கள்", + "move_to_locked_folder_confirmation": "இந்த புகைப்படங்கள் மற்றும் வீடியோ அனைத்து ஆல்பங்களிலிருந்தும் அகற்றப்படும், மேலும் பூட்டப்பட்ட கோப்புறையிலிருந்து மட்டுமே பார்க்க முடியும்", + "moved_to_archive": "நகர்த்தப்பட்டது", + "moved_to_library": "நகர்த்தப்பட்டது", "moved_to_trash": "குப்பைக்கு நகர்த்தப்பட்டது", + "multiselect_grid_edit_date_time_err_read_only": "சொத்து (களை) மட்டுமே படித்த தேதியைத் திருத்த முடியாது, தவிர்க்கவும்", + "multiselect_grid_edit_gps_err_read_only": "சொத்து (களை) மட்டுமே படிக்க, ச்கிப்பிங் ஆகியவற்றைத் திருத்த முடியாது", + "mute_memories": "முடக்கு நினைவுகள்", "my_albums": "எனது ஆல்பங்கள்", "name": "பெயர்", "name_or_nickname": "பெயர் அல்லது புனைப்பெயர்", + "network_requirement_photos_upload": "காப்புப்பிரதி புகைப்படங்களுக்கு செல்லுலார் தரவைப் பயன்படுத்தவும்", + "network_requirement_videos_upload": "காப்புப்பிரதி வீடியோக்களுக்கு செல்லுலார் தரவைப் பயன்படுத்தவும்", + "network_requirements": "பிணைய தேவைகள்", + "network_requirements_updated": "பிணையம் தேவைகள் மாற்றப்பட்டன, காப்புப்பிரதி வரிசையை மீட்டமைக்கிறது", + "networking_settings": "நெட்வொர்க்கிங்", + "networking_subtitle": "சேவையக இறுதிப்புள்ளி அமைப்புகளை நிர்வகிக்கவும்", "never": "ஒருபோதும்", "new_album": "புதிய ஆல்பம்", "new_api_key": "புதிய பநிஇ விசை", "new_password": "புதிய கடவுச்சொல்", "new_person": "புதிய நபர்", + "new_pin_code": "புதிய முள் குறியீடு", + "new_pin_code_subtitle": "பூட்டப்பட்ட கோப்புறையை அணுக இது உங்கள் முதல் முறையாகும். இந்த பக்கத்தை பாதுகாப்பாக அணுக ஒரு முள் குறியீட்டை உருவாக்கவும்", + "new_timeline": "புதிய காலவரிசை", "new_user_created": "புதிய பயனர் உருவாக்கப்பட்டது", "new_version_available": "புதிய பதிப்பு கிடைக்கிறது", "newest_first": "புதிய முதல்", @@ -858,42 +1385,68 @@ "no_albums_yet": "உங்களிடம் இதுவரை எந்த ஆல்பங்களும் இல்லை என்று தெரிகிறது.", "no_archived_assets_message": "உங்கள் புகைப்படக் காட்சியில் இருந்து அவற்றை மறைக்க புகைப்படங்கள் மற்றும் வீடியோக்களை காப்பகப்படுத்தவும்", "no_assets_message": "உங்கள் முதல் புகைப்படத்தை பதிவேற்ற சொடுக்கு செய்க", + "no_assets_to_show": "காட்ட சொத்துக்கள் இல்லை", + "no_cast_devices_found": "நடிகர்கள் சாதனங்கள் எதுவும் கிடைக்கவில்லை", + "no_checksum_local": "செக்சம் எதுவும் கிடைக்கவில்லை - உள்ளக சொத்துக்களைப் பெற முடியாது", + "no_checksum_remote": "செக்சம் எதுவும் கிடைக்கவில்லை - தொலை சொத்து பெற முடியாது", "no_duplicates_found": "நகல்கள் எதுவும் காணப்படவில்லை.", "no_exif_info_available": "EXIF செய்தி எதுவும் கிடைக்கவில்லை", "no_explore_results_message": "உங்கள் தொகுப்பை ஆராய கூடுதல் புகைப்படங்களை பதிவேற்றவும்.", "no_favorites_message": "உங்கள் சிறந்த படங்கள் மற்றும் வீடியோக்களை விரைவாகக் கண்டுபிடிக்க பிடித்தவைகளைச் சேர்க்கவும்", "no_libraries_message": "உங்கள் புகைப்படங்கள் மற்றும் வீடியோக்களைக் காண வெளிப்புற நூலகத்தை உருவாக்கவும்", + "no_local_assets_found": "இந்த செக்சம் மூலம் உள்ளக சொத்துக்கள் எதுவும் காணப்படவில்லை", + "no_locked_photos_message": "பூட்டப்பட்ட கோப்புறையில் உள்ள புகைப்படங்கள் மற்றும் வீடியோக்கள் மறைக்கப்பட்டுள்ளன, மேலும் நீங்கள் உங்கள் நூலகத்தை உலாவும்போது அல்லது தேடும்போது காண்பிக்கப்படாது.", "no_name": "பெயர் இல்லை", + "no_notifications": "அறிவிப்புகள் இல்லை", + "no_people_found": "பொருந்தக்கூடிய நபர்கள் எதுவும் கிடைக்கவில்லை", "no_places": "இடங்கள் இல்லை", + "no_remote_assets_found": "இந்த செக்சம் மூலம் தொலைநிலை சொத்துக்கள் எதுவும் காணப்படவில்லை", "no_results": "முடிவுகள் இல்லை", "no_results_description": "ஒரு ஒத்த அல்லது பொதுவான முக்கிய சொல்லை முயற்சிக்கவும்", "no_shared_albums_message": "உங்கள் நெட்வொர்க்கில் உள்ளவர்களுடன் புகைப்படங்களையும் வீடியோக்களையும் பகிர்ந்து கொள்ள ஒரு ஆல்பத்தை உருவாக்கவும்", + "no_uploads_in_progress": "பதிவேற்றங்கள் முன்னேற்றத்தில் இல்லை", + "not_available": "இதற்கில்லை", "not_in_any_album": "எந்த ஆல்பத்திலும் இல்லை", + "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": "அறிவிப்புகளை இயக்கவும்", + "notification_permission_list_tile_title": "அறிவிப்பு இசைவு", "notification_toggle_setting_description": "மின்னஞ்சல் அறிவிப்புகளை இயக்கவும்", "notifications": "அறிவிப்புகள்", "notifications_setting_description": "அறிவிப்புகளை நிர்வகிக்கவும்", "oauth": "Oauth", "official_immich_resources": "உத்தியோகபூர்வ இம்மா வளங்கள்", "offline": "இணையமில்லாமல்", + "offset": "ஈடுசெய்யும்", "ok": "சரி", "oldest_first": "முதலில் பழமையானது", + "on_this_device": "இந்த சாதனத்தில்", "onboarding": "ஆன் போர்டிங்", - "onboarding_privacy_description": "பின்வரும் (விரும்பினால்) நற்பொருத்தங்கள் வெளிப்புற சேவைகளை நம்பியுள்ளன, மேலும் நிர்வாக அமைப்புகளில் எந்த நேரத்திலும் முடக்கப்படலாம்.", + "onboarding_locale_description": "உங்களுக்கு விருப்பமான மொழியைத் தேர்ந்தெடுக்கவும். இதை உங்கள் அமைப்புகளில் பின்னர் மாற்றலாம்.", + "onboarding_privacy_description": "பின்வரும் (விரும்பினால்) நற்பொருத்தங்கள் வெளிப்புற சேவைகளை நம்பியுள்ளன, மேலும் அமைப்புகளில் எந்த நேரத்திலும் முடக்கப்படலாம்.", + "onboarding_server_welcome_description": "சில பொதுவான அமைப்புகளுடன் உங்கள் நிகழ்வை அமைப்போம்.", "onboarding_theme_description": "உங்கள் உதாரணத்திற்கு வண்ண கருப்பொருளைத் தேர்வுசெய்க. இதை உங்கள் அமைப்புகளில் பின்னர் மாற்றலாம்.", + "onboarding_user_welcome_description": "நீங்கள் தொடங்குவோம்!", "onboarding_welcome_user": "வரவேற்கிறோம், {user}", "online": "ஆன்லைனில்", "only_favorites": "பிடித்தவை மட்டுமே", + "open": "திற", "open_in_map_view": "வரைபடக் காட்சியில் திறந்திருக்கும்", "open_in_openstreetmap": "OpenStreetMap இல் திறந்திருக்கும்", "open_the_search_filters": "தேடல் வடிப்பான்களைத் திறக்கவும்", "options": "விருப்பங்கள்", "or": "அல்லது", + "organize_into_albums": "ஆல்பங்களாக ஒழுங்கமைக்கவும்", + "organize_into_albums_description": "தற்போதைய ஒத்திசைவு அமைப்புகளைப் பயன்படுத்தி ஏற்கனவே உள்ள புகைப்படங்களை ஆல்பங்களில் வைக்கவும்", "organize_your_library": "உங்கள் நூலகத்தை ஒழுங்கமைக்கவும்", "original": "அசல்", "other": "மற்றொன்று", "other_devices": "பிற சாதனங்கள்", + "other_entities": "பிற நிறுவனங்கள்", "other_variables": "பிற மாறிகள்", "owned": "சொந்தமானது", "owner": "உரிமையாளர்", @@ -901,6 +1454,14 @@ "partner_can_access": "{partner} அணுகலாம்", "partner_can_access_assets": "காப்பகப்படுத்தப்பட்ட மற்றும் நீக்கப்பட்டவை தவிர உங்கள் புகைப்படங்கள் மற்றும் வீடியோக்கள் அனைத்தும்", "partner_can_access_location": "உங்கள் புகைப்படங்கள் எடுக்கப்பட்ட இடம்", + "partner_list_user_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": "{கூட்டாளர் your இனி உங்கள் புகைப்படங்களை அணுக முடியாது.", "partner_sharing": "கூட்டாளர் பகிர்வு", "partners": "கூட்டாளர்கள்", "password": "கடவுச்சொல்", @@ -926,10 +1487,24 @@ "permanent_deletion_warning_setting_description": "சொத்துக்களை நிரந்தரமாக நீக்கும்போது ஒரு எச்சரிக்கையைக் காட்டுங்கள்", "permanently_delete": "நிரந்தரமாக நீக்கு", "permanently_delete_assets_count": "நிரந்தரமாக நீக்கு {எண்ணிக்கை, பன்மை, ஒன்று {asset} மற்ற {assets}}", - "permanently_delete_assets_prompt": "நீங்கள் நிச்சயமாக {எண்ணிக்கை, பன்மை, ஒன்று {இந்த சொத்து?} மற்ற {இந்த # சொத்துக்கள்? } அவர்களின்}} ஆல்பம் (கள்) இலிருந்து.", + "permanently_delete_assets_prompt": "நீங்கள் நிச்சயமாக {எண்ணிக்கை, பன்மை, ஒன்று {இந்த சொத்து?} மற்ற {இந்த # சொத்துக்கள்?", "permanently_deleted_asset": "நிரந்தரமாக நீக்கப்பட்ட சொத்து", "permanently_deleted_assets_count": "நிரந்தரமாக நீக்கப்பட்டது {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்}}", + "permission": "இசைவு", + "permission_empty": "உங்கள் இசைவு காலியாக இருக்கக்கூடாது", + "permission_onboarding_back": "பின்", + "permission_onboarding_continue_anyway": "எப்படியும் தொடரவும்", + "permission_onboarding_get_started": "தொடங்கவும்", + "permission_onboarding_go_to_settings": "அமைப்புகளுக்குச் செல்லுங்கள்", + "permission_onboarding_permission_denied": "இசைவு மறுக்கப்பட்டது. இம்மிச்சைப் பயன்படுத்த, அமைப்புகளில் புகைப்படம் மற்றும் வீடியோ அனுமதிகளை வழங்கவும்.", + "permission_onboarding_permission_granted": "இசைவு வழங்கப்பட்டது! நீங்கள் அனைவரும் அமைக்கப்பட்டிருக்கிறீர்கள்.", + "permission_onboarding_permission_limited": "இசைவு லிமிடெட். உங்கள் முழு கேலரி சேகரிப்பையும் நிர்வகிக்கவும் நிர்வகிக்கவும், அமைப்புகளில் புகைப்படம் மற்றும் வீடியோ அனுமதிகளை வழங்கவும்.", + "permission_onboarding_request": "உங்கள் புகைப்படங்கள் மற்றும் வீடியோக்களைக் காண இம்மிச்சுக்கு இசைவு தேவை.", "person": "ஆள்", + "person_age_months": "{மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}} பழையது", + "person_age_year_months": "1 ஆண்டு, {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}} பழையது", + "person_age_years": "{ஆண்டுகள், பன்மை, பிற {# ஆண்டுகள்}} பழையது", + "person_birthdate": "{date} இல் பிறந்தார்", "person_hidden": "{name} {மறைக்கப்பட்ட, தேர்ந்தெடு, உண்மை {(மறைக்கப்பட்ட)} பிற {}}", "photo_shared_all_users": "உங்கள் புகைப்படங்களை எல்லா பயனர்களுடனும் பகிர்ந்து கொண்டதாகத் தெரிகிறது அல்லது பகிர்வதற்கு உங்களிடம் எந்த பயனரும் இல்லை.", "photos": "புகைப்படங்கள்", @@ -937,20 +1512,41 @@ "photos_count": "{எண்ணிக்கை, பன்மை, ஒன்று {{எண்ணிக்கை, எண்} புகைப்படம்} பிற {{எண்ணிக்கை, எண்} புகைப்படங்கள்}}", "photos_from_previous_years": "முந்தைய ஆண்டுகளின் புகைப்படங்கள்", "pick_a_location": "ஒரு இடத்தைத் தேர்ந்தெடுங்கள்", + "pin_code_changed_successfully": "முள் குறியீட்டை வெற்றிகரமாக மாற்றியது", + "pin_code_reset_successfully": "முள் குறியீட்டை வெற்றிகரமாக மீட்டமைக்கவும்", + "pin_code_setup_successfully": "முள் குறியீட்டை வெற்றிகரமாக அமைக்கவும்", + "pin_verification": "குறியீடு சரிபார்ப்பு", "place": "இடம்", "places": "இடங்கள்", + "places_count": "{எண்ணிக்கை, பன்மை, ஒன்று {{எண்ணிக்கை, எண்} இடம்} பிற {{எண்ணிக்கை, எண்} இடங்கள்}}", "play": "விளையாடுங்கள்", "play_memories": "பிளேமெமரிகள்", "play_motion_photo": "இயக்க புகைப்படத்தை விளையாடுங்கள்", "play_or_pause_video": "வீடியோவை இயக்கவும் அல்லது இடைநிறுத்தவும்", + "please_auth_to_access": "அணுகலை அங்கீகரிக்கவும்", "port": "துறைமுகம்", + "preferences_settings_subtitle": "பயன்பாட்டின் விருப்பங்களை நிர்வகிக்கவும்", + "preferences_settings_title": "விருப்பத்தேர்வுகள்", + "preparing": "தயாராகிறது", "preset": "முன்னமைவு", "preview": "முன்னோட்டம்", "previous": "முந்தைய", "previous_memory": "முந்தைய நினைவகம்", - "previous_or_next_photo": "முந்தைய அல்லது அடுத்த புகைப்படம்", + "previous_or_next_day": "நாள் முன்னோக்கி/பின்னோக்கி", + "previous_or_next_month": "மாதம் முன்னோக்கி/பின்னோக்கி", + "previous_or_next_photo": "புகைப்படம் முன்னோக்கி/பின்னோக்கி", + "previous_or_next_year": "ஆண்டு முன்னோக்கி/பின்னோக்கி", "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": "கிட்ஹப்", + "profile_drawer_readonly_mode": "படிக்க மட்டும் பயன்முறை இயக்கப்பட்டது. வெளியேற பயனர் அவதார் ஐகானை நீண்ட நேரம் அழுத்தவும்.", + "profile_drawer_server_out_of_date_major": "சேவையகம் காலாவதியானது. அண்மைக் கால முக்கிய பதிப்பிற்கு புதுப்பிக்கவும்.", + "profile_drawer_server_out_of_date_minor": "சேவையகம் காலாவதியானது. அண்மைக் கால சிறிய பதிப்பிற்கு புதுப்பிக்கவும்.", "profile_image_of_user": "{பயனரின் சுயவிவரப் படம்", "profile_picture_set": "சுயவிவரப் பட தொகுப்பு.", "public_album": "பொது ஆல்பம்", @@ -975,7 +1571,7 @@ "purchase_lifetime_description": "வாழ்நாள் கொள்முதல்", "purchase_option_title": "விருப்பங்களை வாங்கவும்", "purchase_panel_info_1": "இம்மியை உருவாக்குவதற்கு நிறைய நேரமும் முயற்சியும் தேவைப்படுகிறது, மேலும் முழுநேர பொறியியலாளர்கள் அதை எங்களால் முடிந்தவரை சிறப்பாகச் செய்ய வேலை செய்கிறார்கள். எங்கள் நோக்கம் திறந்த மூல மென்பொருள் மற்றும் நெறிமுறை வணிக நடைமுறைகள் டெவலப்பர்களுக்கான நிலையான வருமான ஆதாரமாக மாறுவதும், சுரண்டல் முகில் சேவைகளுக்கு உண்மையான மாற்றுகளுடன் தனியுரிமை-மரியாதைக்குரிய சுற்றுச்சூழல் அமைப்பை உருவாக்குவதும் ஆகும்.", - "purchase_panel_info_2": "பேவால்களைச் சேர்க்காமல் இருப்பதில் நாங்கள் கடமைப்பட்டுள்ளதால், இந்த கொள்முதல் இம்மிச்சில் கூடுதல் அம்சங்களை உங்களுக்கு வழங்காது. இம்மிச்சின் தற்போதைய வளர்ச்சியை ஆதரிக்க உங்களைப் போன்ற பயனர்களை நாங்கள் நம்பியுள்ளோம்.", + "purchase_panel_info_2": "பேவால்களைச் சேர்க்காமல் இருப்பதில் நாங்கள் உறுதியாக இருப்பதால், இந்த கொள்முதல் இம்மிச்சில் கூடுதல் அம்சங்களை உங்களுக்கு வழங்காது. இம்மிச்சின் தற்போதைய வளர்ச்சியை ஆதரிக்க உங்களைப் போன்ற பயனர்களை நாங்கள் நம்பியுள்ளோம்.", "purchase_panel_title": "திட்டத்தை ஆதரிக்கவும்", "purchase_per_server": "ஒரு சேவையகத்திற்கு", "purchase_per_user": "ஒரு பயனருக்கு", @@ -987,19 +1583,28 @@ "purchase_server_description_2": "ஆதரவாளர் நிலை", "purchase_server_title": "சேவையகம்", "purchase_settings_server_activated": "சேவையக தயாரிப்பு விசை நிர்வாகியால் நிர்வகிக்கப்படுகிறது", + "query_asset_id": "வினவல் சொத்து அடையாளம்", + "queue_status": "வரிசை {count}/{total}", "rating": "நட்சத்திர மதிப்பீடு", "rating_clear": "தெளிவான மதிப்பீடு", "rating_count": "{எண்ணிக்கை, பன்மை, ஒன்று {# நட்சத்திரம்} மற்ற {# நட்சத்திரங்கள்}}", "rating_description": "செய்தி குழுவில் EXIF மதிப்பீட்டைக் காண்பி", "reaction_options": "எதிர்வினை விருப்பங்கள்", "read_changelog": "சேஞ்ச்லாக் படிக்கவும்", - "reassign": "மீண்டும் இணைக்கவும்", + "readonly_mode_disabled": "படிக்க மட்டும் பயன்முறை முடக்கப்பட்டுள்ளது", + "readonly_mode_enabled": "படிக்க மட்டும் பயன்முறை இயக்கப்பட்டது", + "ready_for_upload": "பதிவேற்றத் தயார்", + "reassign": "மீண்டும் ஒதுக்கு", "reassigned_assets_to_existing_person": "மீண்டும் ஒதுக்கப்பட்ட {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்}} பெறுநர் {பெயருக்கு, தேர்ந்தெடுக்கவும், சுழிய {an existing person} பிற {{name}}}", "reassigned_assets_to_new_person": "மீண்டும் ஒதுக்கப்பட்ட {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்}} ஒரு புதிய நபருக்கு", "reassing_hint": "தேர்ந்தெடுக்கப்பட்ட சொத்துக்களை ஏற்கனவே இருக்கும் நபருக்கு ஒதுக்குங்கள்", "recent": "அண்மைக் கால", "recent-albums": "அண்மைக் கால ஆல்பங்கள்", "recent_searches": "அண்மைக் கால தேடல்கள்", + "recently_added": "அண்மைக் காலத்தில் சேர்க்கப்பட்டது", + "recently_added_page_title": "அண்மைக் காலத்தில் சேர்க்கப்பட்டது", + "recently_taken": "அண்மைக் காலத்தில் எடுக்கப்பட்டது", + "recently_taken_page_title": "அண்மைக் காலத்தில் எடுக்கப்பட்டது", "refresh": "புதுப்பிப்பு", "refresh_encoded_videos": "குறியிடப்பட்ட வீடியோக்களை புதுப்பிக்கவும்", "refresh_faces": "முகங்களைப் புதுப்பிக்கவும்", @@ -1011,6 +1616,9 @@ "refreshing_faces": "புத்துணர்ச்சியூட்டும் முகங்கள்", "refreshing_metadata": "புத்துணர்ச்சியூட்டும் மேனிலை தரவு", "regenerating_thumbnails": "சிறுபடங்களை மீண்டும் உருவாக்குகிறது", + "remote": "தொலைநிலை", + "remote_assets": "தொலை சொத்துக்கள்", + "remote_media_summary": "தொலை ஊடக சுருக்கம்", "remove": "அகற்று", "remove_assets_album_confirmation": "ஆல்பத்திலிருந்து {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்} your ஐ அகற்ற விரும்புகிறீர்களா?", "remove_assets_shared_link_confirmation": "இந்த பகிரப்பட்ட இணைப்பிலிருந்து {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்} your ஐ அகற்ற விரும்புகிறீர்களா?", @@ -1018,14 +1626,23 @@ "remove_custom_date_range": "தனிப்பயன் தேதி வரம்பை அகற்று", "remove_deleted_assets": "நீக்கப்பட்ட சொத்துக்களை அகற்றவும்", "remove_from_album": "ஆல்பத்திலிருந்து அகற்று", + "remove_from_album_action_prompt": "{எண்ணிக்கை the ஆல்பத்திலிருந்து அகற்றப்பட்டது", "remove_from_favorites": "பிடித்தவைகளிலிருந்து அகற்று", + "remove_from_lock_folder_action_prompt": "{எண்ணிக்கை the பூட்டப்பட்ட கோப்புறையிலிருந்து அகற்றப்பட்டது", + "remove_from_locked_folder": "பூட்டப்பட்ட கோப்புறையிலிருந்து அகற்று", + "remove_from_locked_folder_confirmation": "பூட்டப்பட்ட கோப்புறையிலிருந்து இந்த புகைப்படங்களையும் வீடியோக்களையும் நகர்த்த விரும்புகிறீர்களா? அவை உங்கள் நூலகத்தில் தெரியும்.", "remove_from_shared_link": "பகிரப்பட்ட இணைப்பிலிருந்து அகற்று", + "remove_memory": "நினைவகத்தை அகற்று", + "remove_photo_from_memory": "இந்த நினைவிலிருந்து புகைப்படத்தை அகற்று", + "remove_tag": "குறிச்சொல்லை அகற்று", "remove_url": "முகவரி ஐ அகற்று", "remove_user": "பயனரை அகற்று", "removed_api_key": "அகற்றப்பட்ட பநிஇ விசை: {name}", "removed_from_archive": "காப்பகத்திலிருந்து அகற்றப்பட்டது", "removed_from_favorites": "பிடித்தவைகளிலிருந்து அகற்றப்பட்டது", "removed_from_favorites_count": "{எண்ணிக்கை, பன்மை, பிற {பிடித்தவைகளிலிருந்து #}} அகற்றப்பட்டது", + "removed_memory": "அகற்றப்பட்ட நினைவகம்", + "removed_photo_from_memory": "நினைவிலிருந்து புகைப்படத்தை அகற்றியது", "removed_tagged_assets": "{எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்} இருந்து இலிருந்து அகற்றப்பட்ட குறிச்சொல்", "rename": "மறுபெயரிடுங்கள்", "repair": "பழுது", @@ -1034,27 +1651,41 @@ "repository": "களஞ்சியம்", "require_password": "கடவுச்சொல் தேவை", "require_user_to_change_password_on_first_login": "முதல் உள்நுழைவில் கடவுச்சொல்லை மாற்ற பயனர் தேவை", + "rescan": "ரெச்கான்", "reset": "மீட்டமை", "reset_password": "கடவுச்சொல்லை மீட்டமைக்கவும்", "reset_people_visibility": "மக்களின் தெரிவுநிலையை மீட்டமைக்கவும்", + "reset_pin_code": "முள் குறியீட்டை மீட்டமைக்கவும்", + "reset_pin_code_description": "உங்கள் முள் குறியீட்டை மறந்துவிட்டால், அதை மீட்டமைக்க சேவையக நிர்வாகியை தொடர்பு கொள்ளலாம்", + "reset_pin_code_success": "முள் குறியீட்டை வெற்றிகரமாக மீட்டமைக்கவும்", + "reset_pin_code_with_password": "உங்கள் கடவுச்சொல் மூலம் உங்கள் முள் குறியீட்டை எப்போதும் மீட்டமைக்கலாம்", + "reset_sqlite": "SQLite தரவுத்தளத்தை மீட்டமைக்கவும்", + "reset_sqlite_confirmation": "SQLITE தரவுத்தளத்தை மீட்டமைக்க விரும்புகிறீர்களா? தரவை மீண்டும் ஒத்திசைக்க நீங்கள் வெளியேறி மீண்டும் உள்நுழைய வேண்டும்", + "reset_sqlite_success": "SQLITE தரவுத்தளத்தை வெற்றிகரமாக மீட்டமைக்கவும்", "reset_to_default": "இயல்புநிலைக்கு மீட்டமைக்கவும்", "resolve_duplicates": "நகல்களைத் தீர்க்கவும்", "resolved_all_duplicates": "அனைத்து நகல்களையும் தீர்க்கும்", "restore": "மீட்டமை", "restore_all": "அனைத்தையும் மீட்டெடுக்கவும்", + "restore_trash_action_prompt": "{எண்ணிக்கை the குப்பைகளிலிருந்து மீட்டெடுக்கப்பட்டது", "restore_user": "பயனரை மீட்டமைக்கவும்", "restored_asset": "மீட்டெடுக்கப்பட்ட சொத்து", "resume": "மீண்டும் தொடங்குங்கள்", + "resume_paused_jobs": "மீண்டும் தொடங்குங்கள் {எண்ணிக்கை, பன்மை, ஒன்று {# இடைநிறுத்தப்பட்ட வேலை} மற்ற {# இடைநிறுத்தப்பட்ட வேலைகள்}}", "retry_upload": "பதிவேற்ற முயற்சிக்கவும்", "review_duplicates": "நகல்களை மதிப்பாய்வு செய்யவும்", + "review_large_files": "பெரிய கோப்புகளை மதிப்பாய்வு செய்யவும்", "role": "பங்கு", "role_editor": "திருத்தி", "role_viewer": "பார்வையாளர்", + "running": "இயங்கும்", "save": "சேமி", + "save_to_gallery": "கேலரியில் சேமிக்கவும்", "saved_api_key": "சேமித்த பநிஇ விசை", "saved_profile": "சேமித்த சுயவிவரம்", "saved_settings": "சேமித்த அமைப்புகள்", "say_something": "ஏதாவது சொல்லுங்கள்", + "scaffold_body_error_occurred": "பிழை ஏற்பட்டது", "scan_all_libraries": "அனைத்து நூலகங்களையும் ச்கேன் செய்யுங்கள்", "scan_library": "ச்கேன்", "scan_settings": "அமைப்புகளை ச்கேன் செய்யுங்கள்", @@ -1062,20 +1693,53 @@ "search": "தேடல்", "search_albums": "ஆல்பங்களைத் தேடுங்கள்", "search_by_context": "சூழலால் தேடுங்கள்", + "search_by_description": "விளக்கத்தின் மூலம் தேடுங்கள்", + "search_by_description_example": "சப்பாவில் நடைபயணம்", "search_by_filename": "கோப்பு பெயர் அல்லது நீட்டிப்பு மூலம் தேடுங்கள்", "search_by_filename_example": "I.E. IMG_1234.JPG அல்லது PNG", "search_camera_make": "தேடல் கேமரா செய்யுங்கள் ...", "search_camera_model": "கேமரா மாதிரியைத் தேடுங்கள் ...", "search_city": "தேடல் நகரம் ...", "search_country": "தேடல் நாடு ...", + "search_filter_apply": "வடிகட்டியைப் பயன்படுத்துங்கள்", + "search_filter_camera_title": "கேமரா வகையைத் தேர்ந்தெடுக்கவும்", + "search_filter_date": "திகதி", + "search_filter_date_interval": "{start} பெறுநர் {end}", + "search_filter_date_title": "தேதி வரம்பைத் தேர்ந்தெடுக்கவும்", + "search_filter_display_option_not_in_album": "ஆல்பத்தில் இல்லை", + "search_filter_display_options": "காட்சி விருப்பங்கள்", + "search_filter_filename": "கோப்பு பெயர் மூலம் தேடுங்கள்", + "search_filter_location": "இடம்", + "search_filter_location_title": "இருப்பிடத்தைத் தேர்ந்தெடுக்கவும்", + "search_filter_media_type": "ஊடக வகை", + "search_filter_media_type_title": "மீடியா வகையைத் தேர்ந்தெடுக்கவும்", + "search_filter_people_title": "மக்களைத் தேர்ந்தெடுக்கவும்", + "search_for": "தேடுங்கள்", "search_for_existing_person": "இருக்கும் நபரைத் தேடுங்கள்", + "search_no_more_result": "மேலும் முடிவுகள் இல்லை", "search_no_people": "மக்கள் இல்லை", "search_no_people_named": "\"{name}\" என்று பெயரிடப்பட்டவர்கள் யாரும் இல்லை", + "search_no_result": "முடிவுகள் எதுவும் கிடைக்கவில்லை, வேறு தேடல் காலத்தை அல்லது கலவையை முயற்சிக்கவும்", "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_page_selfies": "செல்ஃபிகள்", + "search_page_things": "விசயங்கள்", + "search_page_view_all_button": "அனைத்தையும் காண்க", + "search_page_your_activity": "உங்கள் செயல்பாடு", + "search_page_your_map": "உங்கள் வரைபடம்", "search_people": "மக்களைத் தேடுங்கள்", "search_places": "இடங்களைத் தேடுங்கள்", + "search_rating": "மதிப்பீட்டின் மூலம் தேடுங்கள் ...", + "search_result_page_new_search_hint": "புதிய தேடல்", "search_settings": "அமைப்புகளைத் தேடுங்கள்", "search_state": "தேடல் நிலை ...", + "search_suggestion_list_smart_search_hint_1": "மெட்டாடேட்டாவைத் தேட, நிகழ்வைப் பயன்படுத்த, அறிவுள்ள தேடல் இயல்புநிலையாக இயக்கப்பட்டது ", + "search_suggestion_list_smart_search_hint_2": "எம்: உங்கள் தேடல்-காலநிலை", "search_tags": "குறிச்சொற்களைத் தேடுங்கள் ...", "search_timezone": "நேர மண்டலத்தைத் தேடுங்கள் ...", "search_type": "தேடல் வகை", @@ -1083,9 +1747,11 @@ "searching_locales": "இடங்களைத் தேடுகிறது ...", "second": "இரண்டாவது", "see_all_people": "எல்லா மக்களையும் பாருங்கள்", + "select": "தேர்ந்தெடு", "select_album_cover": "ஆல்பம் அட்டையைத் தேர்ந்தெடுக்கவும்", "select_all": "அனைத்தையும் தெரிவுசெய்", "select_all_duplicates": "அனைத்து நகல்களையும் தேர்ந்தெடுக்கவும்", + "select_all_in": "{குழுவில் அனைத்தையும் தேர்ந்தெடுக்கவும்", "select_avatar_color": "அவதார் நிறத்தைத் தேர்ந்தெடுக்கவும்", "select_face": "முகத்தைத் தேர்ந்தெடுக்கவும்", "select_featured_photo": "பிரத்யேக புகைப்படத்தைத் தேர்ந்தெடுக்கவும்", @@ -1093,14 +1759,21 @@ "select_keep_all": "அனைத்தையும் வைத்திருங்கள் என்பதைத் தேர்ந்தெடுக்கவும்", "select_library_owner": "நூலக உரிமையாளரைத் தேர்ந்தெடுக்கவும்", "select_new_face": "புதிய முகத்தைத் தேர்ந்தெடுக்கவும்", + "select_person_to_tag": "குறிக்க ஒரு நபரைத் தேர்ந்தெடுக்கவும்", "select_photos": "புகைப்படங்களைத் தேர்ந்தெடுக்கவும்", "select_trash_all": "குப்பைத் தொட்டியைத் தேர்ந்தெடுக்கவும்", + "select_user_for_sharing_page_err_album": "ஆல்பத்தை உருவாக்கத் தவறிவிட்டது", "selected": "தேர்ந்தெடுக்கப்பட்டது", "selected_count": "{எண்ணிக்கை, பன்மை, பிற {# தேர்ந்தெடுக்கப்பட்ட}}", + "selected_gps_coordinates": "தேர்ந்தெடுக்கப்பட்ட சி.பி.எச் ஆயத்தொலைவுகள்", "send_message": "செய்தி அனுப்பவும்", "send_welcome_email": "வரவேற்பு மின்னஞ்சலை அனுப்பவும்", + "server_endpoint": "சேவையக இறுதிப்புள்ளி", + "server_info_box_app_version": "பயன்பாட்டு பதிப்பு", + "server_info_box_server_url": "சேவையக முகவரி", "server_offline": "சேவையகம் இணைப்பில்லாத", "server_online": "ஆன்லைனில் சேவையகம்", + "server_privacy": "சேவையக தனியுரிமை", "server_stats": "சேவையக புள்ளிவிவரங்கள்", "server_version": "சேவையக பதிப்பு", "set": "கணம்", @@ -1110,21 +1783,96 @@ "set_date_of_birth": "பிறந்த தேதியை அமைக்கவும்", "set_profile_picture": "சுயவிவரப் படத்தை அமைக்கவும்", "set_slideshow_to_fullscreen": "ச்லைடுசோவை முழுமைக்கு அமைக்கவும்", + "set_stack_primary_asset": "முதன்மை சொத்தாக அமைக்கவும்", + "setting_image_viewer_help": "விவரம் பார்வையாளர் முதலில் சிறிய சிறு உருவத்தை ஏற்றுகிறார், பின்னர் நடுத்தர அளவிலான முன்னோட்டத்தை ஏற்றுகிறார் (இயக்கப்பட்டால்), இறுதியாக அசலை ஏற்றுகிறது (இயக்கப்பட்டால்).", + "setting_image_viewer_original_subtitle": "அசல் முழு தெளிவுத்திறன் படத்தை ஏற்றவும் (பெரியது!). தரவு பயன்பாட்டைக் குறைக்க முடக்கு (பிணையம் மற்றும் சாதன தற்காலிக சேமிப்பு இரண்டும்).", + "setting_image_viewer_original_title": "அசல் படத்தை ஏற்றவும்", + "setting_image_viewer_preview_subtitle": "நடுத்தர-தெளிவுத்திறன் படத்தை ஏற்றவும். அசலை நேரடியாக ஏற்ற முடக்கவும் அல்லது சிறுபடத்தை மட்டுமே பயன்படுத்தவும்.", + "setting_image_viewer_preview_title": "முன்னோட்டம் படத்தை ஏற்றவும்", + "setting_image_viewer_title": "படங்கள்", + "setting_languages_apply": "இடு", + "setting_languages_subtitle": "பயன்பாட்டின் மொழியை மாற்றவும்", + "setting_notifications_notify_failures_grace_period": "பின்னணி காப்புப்பிரதி தோல்விகளை அறிவிக்கவும்: {duration}", + "setting_notifications_notify_hours": "{count} மணிநேரம்", + "setting_notifications_notify_immediately": "உடனடியாக", + "setting_notifications_notify_minutes": "{count} நிமிடங்கள்", + "setting_notifications_notify_never": "ஒருபோதும்", + "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": "சேவையகத்திலிருந்து ஒரு வீடியோவை ச்ட்ரீமிங் செய்யும் போது, ஒரு டிரான்ச்கோடு கிடைக்கும்போது கூட அசலை இயக்கவும். இடையகத்திற்கு வழிவகுக்கும். இந்த அமைப்பைப் பொருட்படுத்தாமல் உள்நாட்டில் கிடைக்கும் வீடியோக்கள் அசல் தரத்தில் இயக்கப்படுகின்றன.", + "setting_video_viewer_original_video_title": "அசல் வீடியோவை கட்டாயப்படுத்துங்கள்", "settings": "அமைப்புகள்", + "settings_require_restart": "இந்த அமைப்பைப் பயன்படுத்த இம்மியை மறுதொடக்கம் செய்யுங்கள்", "settings_saved": "அமைப்புகள் சேமிக்கப்பட்டன", + "setup_pin_code": "முள் குறியீட்டை அமைக்கவும்", "share": "பங்கு", + "share_action_prompt": "பகிரப்பட்ட {count} சொத்துக்கள்", + "share_add_photos": "புகைப்படங்களைச் சேர்க்கவும்", + "share_assets_selected": "{count} தேர்ந்தெடுக்கப்பட்டது", + "share_dialog_preparing": "தயாரித்தல் ...", + "share_link": "இணைப்பைப் பகிரவும்", "shared": "பகிரப்பட்டது", + "shared_album_activities_input_disable": "கருத்து முடக்கப்பட்டுள்ளது", + "shared_album_activity_remove_content": "இந்தச் செயல்பாட்டை நீக்க விரும்புகிறீர்களா?", + "shared_album_activity_remove_title": "செயல்பாட்டை நீக்கு", + "shared_album_section_people_action_error": "ஆல்பத்திலிருந்து வெளியேறுதல்/நீக்குதல்", + "shared_album_section_people_action_leave": "ஆல்பத்திலிருந்து பயனரை அகற்று", + "shared_album_section_people_action_remove_user": "ஆல்பத்திலிருந்து பயனரை அகற்று", + "shared_album_section_people_title": "மக்கள்", "shared_by": "பகிரப்பட்டது", "shared_by_user": "{பயனரால் பகிரப்பட்டது", "shared_by_you": "நீங்கள் பகிர்ந்து கொண்டார்", "shared_from_partner": "{partner} இலிருந்து புகைப்படங்கள்", + "shared_intent_upload_button_progress_text": "{current} / {total} பதிவேற்றப்பட்டது", + "shared_link_app_bar_title": "பகிரப்பட்ட இணைப்புகள்", + "shared_link_clipboard_copied_massage": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", + "shared_link_clipboard_text": "இணைப்பு: {link} \nகடவுச்சொல்: {password}", + "shared_link_create_error": "பகிரப்பட்ட இணைப்பை உருவாக்கும் போது பிழை", + "shared_link_custom_url_description": "தனிப்பயன் முகவரி உடன் இந்த பகிரப்பட்ட இணைப்பை அணுகவும்", + "shared_link_edit_description_hint": "பங்கு விளக்கத்தை உள்ளிடவும்", + "shared_link_edit_expire_after_option_day": "1 நாள்", + "shared_link_edit_expire_after_option_days": "{count} நாட்கள்", + "shared_link_edit_expire_after_option_hour": "1 மணி நேரம்", + "shared_link_edit_expire_after_option_hours": "{count} மணிநேரம்", + "shared_link_edit_expire_after_option_minute": "1 மணித்துளி", + "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": "{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": "{count} இரண்டாவதாக காலாவதியாகிறது", + "shared_link_expires_seconds": "{count} விநாடிகளில் காலாவதியாகிறது", + "shared_link_individual_shared": "தனிநபர் பகிரப்பட்டவர்", + "shared_link_info_chip_metadata": "Exif", + "shared_link_manage_links": "பகிரப்பட்ட இணைப்புகளை நிர்வகிக்கவும்", "shared_link_options": "பகிரப்பட்ட இணைப்பு விருப்பங்கள்", + "shared_link_password_description": "இந்த பகிரப்பட்ட இணைப்பை அணுக கடவுச்சொல் தேவை", "shared_links": "பகிரப்பட்ட இணைப்புகள்", + "shared_links_description": "புகைப்படங்கள் மற்றும் வீடியோக்களை இணைப்புடன் பகிரவும்", "shared_photos_and_videos_count": "{ASSETCOUNT, பன்மை, பிற {# பகிரப்பட்ட புகைப்படங்கள் மற்றும் வீடியோக்கள்.}}", + "shared_with_me": "என்னுடன் பகிரப்பட்டது", "shared_with_partner": "{கூட்டாளர் with உடன் பகிரப்பட்டது", "sharing": "பகிர்வு", "sharing_enter_password": "இந்த பக்கத்தைக் காண கடவுச்சொல்லை உள்ளிடவும்.", + "sharing_page_album": "பகிரப்பட்ட ஆல்பங்கள்", + "sharing_page_description": "உங்கள் நெட்வொர்க்கில் உள்ளவர்களுடன் புகைப்படங்களையும் வீடியோக்களையும் பகிர்ந்து கொள்ள பகிரப்பட்ட ஆல்பங்களை உருவாக்கவும்.", + "sharing_page_empty_list": "வெற்று பட்டியல்", "sharing_sidebar_description": "பக்கப்பட்டியில் பகிர்வதற்கான இணைப்பைக் காண்பி", + "sharing_silver_appbar_create_shared_album": "புதிய பகிரப்பட்ட ஆல்பம்", + "sharing_silver_appbar_share_partner": "கூட்டாளருடன் பகிர்ந்து கொள்ளுங்கள்", "shift_to_permanent_delete": "சொத்தை நிரந்தரமாக நீக்க ⇧ ஐ அழுத்தவும்", "show_album_options": "ஆல்பம் விருப்பங்களைக் காட்டு", "show_albums": "ஆல்பங்களைக் காட்டு", @@ -1142,9 +1890,11 @@ "show_person_options": "நபர் விருப்பங்களைக் காட்டு", "show_progress_bar": "முன்னேற்றப் பட்டியைக் காட்டு", "show_search_options": "தேடல் விருப்பங்களைக் காட்டு", + "show_shared_links": "பகிரப்பட்ட இணைப்புகளைக் காட்டு", "show_slideshow_transition": "ச்லைடுசோ மாற்றத்தைக் காட்டு", "show_supporter_badge": "ஆதரவாளர் ஒட்டு", "show_supporter_badge_description": "ஒரு ஆதரவாளர் பேட்சைக் காட்டு", + "show_text_search_menu": "உரை தேடல் மெனுவைக் காட்டு", "shuffle": "கலக்கு", "sidebar": "பக்கப்பட்டி", "sidebar_display_description": "பக்கப்பட்டியில் பார்வைக்கு ஒரு இணைப்பைக் காண்பி", @@ -1160,12 +1910,14 @@ "sort_created": "தேதி உருவாக்கப்பட்டது", "sort_items": "பொருட்களின் எண்ணிக்கை", "sort_modified": "தேதி மாற்றியமைக்கப்பட்டது", + "sort_newest": "புதிய புகைப்படம்", "sort_oldest": "பழமையான புகைப்படம்", "sort_people_by_similarity": "ஒற்றுமையால் மக்களை வரிசைப்படுத்துங்கள்", "sort_recent": "மிக அண்மைக் கால புகைப்படம்", "sort_title": "தலைப்பு", "source": "மூலம்", "stack": "அடுக்கு", + "stack_action_prompt": "{count} அடுக்கி வைக்கப்பட்டுள்ளது", "stack_duplicates": "அடுக்கு நகல்கள்", "stack_select_one_photo": "அடுக்குக்கு ஒரு முக்கிய புகைப்படத்தைத் தேர்ந்தெடுக்கவும்", "stack_selected_photos": "தேர்ந்தெடுக்கப்பட்ட புகைப்படங்களை அடுக்கி வைக்கவும்", @@ -1173,16 +1925,20 @@ "stacktrace": "ச்டாக் ட்ரேச்", "start": "தொடங்கு", "start_date": "தொடக்க தேதி", + "start_date_before_end_date": "தொடக்க தேதி இறுதி தேதிக்கு முன் இருக்க வேண்டும்", "state": "மாநிலம்", "status": "நிலை", + "stop_casting": "வார்ப்பதை நிறுத்துங்கள்", "stop_motion_photo": "இயக்க புகைப்படத்தை நிறுத்து", "stop_photo_sharing": "உங்கள் புகைப்படங்களைப் பகிர்வதை நிறுத்தவா?", "stop_photo_sharing_description": "{கூட்டாளர் your இனி உங்கள் புகைப்படங்களை அணுக முடியாது.", "stop_sharing_photos_with_user": "இந்த பயனருடன் உங்கள் புகைப்படங்களைப் பகிர்வதை நிறுத்துங்கள்", "storage": "சேமிப்பக இடம்", "storage_label": "சேமிப்பக சிட்டை", + "storage_quota": "சேமிப்பக ஒதுக்கீடு", "storage_usage": "{used} பயன்படுத்தப்படுகிறது", "submit": "சமர்ப்பிக்கவும்", + "success": "செய்", "suggestions": "பரிந்துரைகள்", "sunrise_on_the_beach": "கடற்கரையில் சூரிய தோன்றுகை", "support": "உதவி", @@ -1190,18 +1946,40 @@ "support_third_party_description": "உங்கள் இம்மிச் நிறுவல் மூன்றாம் தரப்பினரால் தொகுக்கப்பட்டது. நீங்கள் அனுபவிக்கும் சிக்கல்கள் அந்த தொகுப்பால் ஏற்படலாம், எனவே கீழேயுள்ள இணைப்புகளைப் பயன்படுத்தி முதல் சந்தர்ப்பத்தில் அவர்களுடன் சிக்கல்களை எழுப்புங்கள்.", "swap_merge_direction": "ஒன்றிணைக்கும் திசையை மாற்றவும்", "sync": "ஒத்திசைவு", + "sync_albums": "ஆல்பங்களை ஒத்திசைக்கவும்", + "sync_albums_manual_subtitle": "பதிவேற்றிய அனைத்து வீடியோக்களையும் புகைப்படங்களையும் தேர்ந்தெடுக்கப்பட்ட காப்பு ஆல்பங்களில் ஒத்திசைக்கவும்", + "sync_local": "உள்ளக ஒத்திசைக்கவும்", + "sync_remote": "தொலைதூரத்தை ஒத்திசைக்கவும்", + "sync_status": "நிலையை ஒத்திசைக்கவும்", + "sync_status_subtitle": "ஒத்திசைவு அமைப்பை பார்வையிடவும் மற்றும் நிர்வகிக்கவும்", + "sync_upload_album_setting_subtitle": "உங்கள் புகைப்படங்களையும் வீடியோக்களையும் இம்மிச்சில் தேர்ந்தெடுக்கப்பட்ட ஆல்பங்களுக்கு உருவாக்கி பதிவேற்றவும்", "tag": "குறிச்சொல்", "tag_assets": "குறிச்சொல் சொத்துக்கள்", "tag_created": "உருவாக்கப்பட்ட குறிச்சொல்: {tag}", "tag_feature_description": "தர்க்கரீதியான குறிச்சொல் தலைப்புகளால் தொகுக்கப்பட்ட புகைப்படங்கள் மற்றும் வீடியோக்களை உலாவுதல்", "tag_not_found_question": "குறிச்சொல்லைக் கண்டுபிடிக்க முடியவில்லையா? <இணைப்பு> புதிய குறிச்சொல்லை உருவாக்கவும். ", + "tag_people": "மக்களை குறிக்கவும்", "tag_updated": "புதுப்பிக்கப்பட்ட குறிச்சொல்: {tag}", "tagged_assets": "குறித்துள்ளார் {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} மற்ற {# சொத்துக்கள்}}", "tags": "குறிச்சொற்கள்", + "tap_to_run_job": "வேலையை இயக்க தட்டவும்", "template": "வார்ப்புரு", "theme": "கருப்பொருள்", "theme_selection": "கருப்பொருள் தேர்வு", "theme_selection_description": "உங்கள் உலாவியின் கணினி விருப்பத்தின் அடிப்படையில் தானாகவே கருப்பொருள் ஒளி அல்லது இருட்டாக அமைக்கவும்", + "theme_setting_asset_list_storage_indicator_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": "விவரம் பட பார்வையாளரின் தரத்தை சரிசெய்யவும்", + "theme_setting_image_viewer_quality_title": "பட பார்வையாளர் தகுதி", + "theme_setting_primary_color_subtitle": "முதன்மை செயல்கள் மற்றும் உச்சரிப்புகளுக்கு ஒரு வண்ணத்தைத் தேர்ந்தெடுங்கள்.", + "theme_setting_primary_color_title": "முதன்மை நிறம்", + "theme_setting_system_primary_color_title": "கணினி நிறத்தைப் பயன்படுத்துங்கள்", + "theme_setting_system_theme_switch": "தானியங்கி (கணினி அமைப்பைப் பின்பற்றவும்)", + "theme_setting_theme_subtitle": "பயன்பாட்டின் கருப்பொருள் அமைப்பைத் தேர்வுசெய்க", + "theme_setting_three_stage_loading_subtitle": "மூன்று-நிலை ஏற்றுதல் ஏற்றுதல் செயல்திறனை அதிகரிக்கக்கூடும், ஆனால் கணிசமாக அதிக பிணைய சுமையை ஏற்படுத்துகிறது", + "theme_setting_three_stage_loading_title": "மூன்று-நிலை ஏற்றுதலை இயக்கவும்", "they_will_be_merged_together": "அவர்கள் ஒன்றாக இணைக்கப்படுவார்கள்", "third_party_resources": "மூன்றாம் தரப்பு வளங்கள்", "time_based_memories": "நேர அடிப்படையிலான நினைவுகள்", @@ -1211,53 +1989,91 @@ "to_change_password": "கடவுச்சொல்லை மாற்றவும்", "to_favorite": "பிடித்த", "to_login": "புகுபதிவு", + "to_multi_select": "பல-தேர்ந்தெடுக்கப்பட்ட", "to_parent": "பெற்றோரிடம் செல்லுங்கள்", + "to_select": "தேர்ந்தெடுக்க", "to_trash": "குப்பை", "toggle_settings": "அமைப்புகளை மாற்றவும்", "total": "மொத்தம்", "total_usage": "மொத்த பயன்பாடு", "trash": "குப்பை", + "trash_action_prompt": "{count} குப்பைக்கு நகர்த்தப்பட்டது", "trash_all": "அனைத்தையும் குப்பை", "trash_count": "குப்பை {எண்ணிக்கை, எண்}", "trash_delete_asset": "குப்பை/சொத்தை நீக்கு", + "trash_emptied": "காலியாக குப்பை", "trash_no_results_message": "குப்பைத் தொட்டிகள் மற்றும் வீடியோக்கள் இங்கே காண்பிக்கப்படும்.", + "trash_page_delete_all": "அனைத்தையும் நீக்கு", + "trash_page_empty_trash_dialog_content": "உங்கள் குப்பை சொத்துக்களை வெறுமை செய்ய விரும்புகிறீர்களா? இந்த உருப்படிகள் இம்மிச்சிலிருந்து நிரந்தரமாக அகற்றப்படும்", + "trash_page_info": "குப்பைத் தொட்டிகள் {days} நாட்களுக்குப் பிறகு நிரந்தரமாக நீக்கப்படும்", + "trash_page_no_assets": "குப்பை சொத்துக்கள் இல்லை", + "trash_page_restore_all": "அனைத்தையும் மீட்டெடுக்கவும்", + "trash_page_select_assets_btn": "சொத்துக்களைத் தேர்ந்தெடுக்கவும்", + "trash_page_title": "({count})", "trashed_items_will_be_permanently_deleted_after": "{நாட்கள், பன்மை, ஒன்று {# நாள்} பிற {# நாட்கள்}} க்குப் பிறகு குப்பைத் தொட்டிகள் நிரந்தரமாக நீக்கப்படும்.", + "troubleshoot": "சரிசெய்தல்", "type": "வகை", + "unable_to_change_pin_code": "முள் குறியீட்டை மாற்ற முடியவில்லை", + "unable_to_setup_pin_code": "முள் குறியீட்டை அமைக்க முடியவில்லை", "unarchive": "அன்கான்", + "unarchive_action_prompt": "{எண்ணிக்கை the காப்பகத்திலிருந்து அகற்றப்பட்டது", "unarchived_count": "{எண்ணிக்கை, பன்மை, பிற {அல்லாத #}}", + "undo": "செயல்தவிர்", "unfavorite": "மாறாத", + "unfavorite_action_prompt": "{எண்ணிக்கை the பிடித்தவைகளிலிருந்து அகற்றப்பட்டது", "unhide_person": "அருவருப்பான நபர்", "unknown": "தெரியவில்லை", + "unknown_country": "தெரியாத நாடு", "unknown_year": "தெரியாத ஆண்டு", "unlimited": "வரம்பற்றது", "unlink_motion_video": "இயக்க வீடியோவை இணைக்கவும்", "unlink_oauth": "OAUTH ஐ இணைக்கவும்", "unlinked_oauth_account": "இணைக்கப்படாத OAUTH கணக்கு", + "unmute_memories": "ஊடுருவல் நினைவுகள்", "unnamed_album": "பெயரிடப்படாத ஆல்பம்", "unnamed_album_delete_confirmation": "இந்த ஆல்பத்தை நீக்க விரும்புகிறீர்களா?", "unnamed_share": "பெயரிடப்படாத பங்கு", "unsaved_change": "சேமிக்கப்படாத மாற்றம்", "unselect_all": "அனைத்தையும் தேர்வு செய்யுங்கள்", "unselect_all_duplicates": "அனைத்து நகல்களையும் தேர்ந்தெடுக்கவும்", + "unselect_all_in": "{group}", "unstack": "அன்-ச்டாக்", + "unstack_action_prompt": "{count} தடையின்றி", "unstacked_assets_count": "அன்-ச்டாக் {எண்ணிக்கை, பன்மை, ஒன்று {# சொத்து} பிற {# சொத்துக்கள்}}", + "untagged": "அவிழ்க்கப்படாதது", "up_next": "அடுத்து", + "update_location_action_prompt": "{count} தேர்ந்தெடுக்கப்பட்ட சொத்துக்களின் இருப்பிடத்தைப் புதுப்பிக்கவும்:", + "updated_at": "புதுப்பிக்கப்பட்டது", "updated_password": "புதுப்பிக்கப்பட்ட கடவுச்சொல்", "upload": "பதிவேற்றும்", + "upload_action_prompt": "{count} பதிவேற்றுவதற்கு வரிசையில் நிற்கப்பட்டது", "upload_concurrency": "ஒத்திசைவைப் பதிவேற்றவும்", + "upload_details": "விவரங்களை பதிவேற்றவும்", + "upload_dialog_info": "தேர்ந்தெடுக்கப்பட்ட சொத்து (களை) சேவையகத்திற்கு காப்புப் பிரதி எடுக்க விரும்புகிறீர்களா?", + "upload_dialog_title": "சொத்தை பதிவேற்றவும்", "upload_errors": "பதிவேற்றம் {எண்ணிக்கை, பன்மை, ஒன்று {# பிழை} மற்ற {# பிழைகள்}} உடன் முடிக்கப்பட்டது, புதிய பதிவேற்ற சொத்துக்களைக் காண பக்கத்தைப் புதுப்பிக்கவும்.", + "upload_finished": "பதிவேற்றம் முடிந்தது", "upload_progress": "மீதமுள்ள {மீதமுள்ள, எண்} - செயலாக்கப்பட்ட {செயலாக்கப்பட்டது, எண்}/{மொத்தம், எண்}", "upload_skipped_duplicates": "{எண்ணிக்கை, பன்மை, ஒன்று {# நகல் சொத்து} பிற {# நகல் சொத்துக்கள்}}", "upload_status_duplicates": "நகல்கள்", "upload_status_errors": "பிழைகள்", "upload_status_uploaded": "பதிவேற்றப்பட்டது", "upload_success": "வெற்றியைப் பதிவேற்றவும், புதிய பதிவேற்ற சொத்துக்களைக் காண பக்கத்தைப் புதுப்பிக்கவும்.", + "upload_to_immich": "இம்மிச்சிற்கு பதிவேற்று ({count})", + "uploading": "பதிவேற்றுகிறது", + "uploading_media": "மீடியாவைப் பதிவேற்றுகிறது", "url": "முகவரி", "usage": "பயன்பாடு", + "use_biometric": "பயோமெட்ரிக்கைப் பயன்படுத்தவும்", + "use_current_connection": "தற்போதைய இணைப்பைப் பயன்படுத்தவும்", "use_custom_date_range": "அதற்கு பதிலாக தனிப்பயன் தேதி வரம்பைப் பயன்படுத்தவும்", "user": "பயனர்", + "user_has_been_deleted": "இந்தப் பயனர் நீக்கப்பட்டார்.", "user_id": "பயனர் ஐடி", "user_liked": "{user} விரும்பினார் {வகை, தேர்ந்தெடு, புகைப்படம் {this photo} வீடியோ {this video} சொத்து {this asset} பிற {it}}", + "user_pin_code_settings": "பின் குறியீடு", + "user_pin_code_settings_description": "உங்கள் பின் குறியீட்டை நிர்வகிக்கவும்", + "user_privacy": "பயனர் தனியுரிமை", "user_purchase_settings": "வாங்க", "user_purchase_settings_description": "உங்கள் வாங்குதலை நிர்வகிக்கவும்", "user_role_set": "{user} {பாத்திரமாக அமைக்கவும்", @@ -1266,12 +2082,14 @@ "user_usage_stats_description": "கணக்கு உபயோகப் புள்ளிவிவரங்களைப் பார்க்க", "username": "பயனர்பெயர்", "users": "பயனர்கள்", + "users_added_to_album_count": "{எண்ணிக்கை, பன்மை, ஒன்று {# பயனர்} மற்ற {# பயனர்கள்}} ஆல்பத்தில் சேர்க்கப்பட்டது", "utilities": "பயன்பாடுகள்", "validate": "சரிபார்க்கவும்", + "validate_endpoint_error": "தயவுசெய்து ஒரு செல்லுபடியாகும் URL ஐ உள்ளிடவும்", "variables": "மாறிகள்", "version": "பதிப்பு", "version_announcement_closing": "உங்கள் நண்பர், அலெக்ச்", - "version_announcement_message": "ஆய்! இம்மியின் புதிய பதிப்பு கிடைக்கிறது. எந்தவொரு தவறான கருத்துக்களையும் தடுக்க உங்கள் அமைப்பு புதுப்பித்த நிலையில் இருப்பதை உறுதிசெய்ய <இணைப்பு> வெளியீட்டுக் குறிப்புகள் ஐப் படிக்க சிறிது நேரம் ஒதுக்குங்கள், குறிப்பாக நீங்கள் காவற்கோபுரத்தைப் பயன்படுத்தினால் அல்லது உங்கள் இம்மிச் நிகழ்வை தானாகவே புதுப்பிப்பதைக் கையாளும் எந்தவொரு பொறிமுறையையும் பயன்படுத்தினால்.", + "version_announcement_message": "வணக்கம்! இம்மியின் புதிய பதிப்பு கிடைக்கிறது. எந்தவொரு தவறான கருத்துக்களையும் தடுக்க உங்கள் அமைப்பு புதுப்பித்த நிலையில் இருப்பதை உறுதிசெய்ய வெளியீட்டுக் குறிப்புகள் ஐப் படிக்க சிறிது நேரம் ஒதுக்குங்கள், குறிப்பாக நீங்கள் காவற்கோபுரத்தைப் பயன்படுத்தினால் அல்லது உங்கள் இம்மிச் நிகழ்வை தானாகவே புதுப்பிப்பதைக் கையாளும் எந்தவொரு பொறிமுறையையும் பயன்படுத்தினால்.", "version_history": "பதிப்பு வரலாறு", "version_history_item": "{version} இல் {date} நிறுவப்பட்டது", "video": "ஒளிதோற்றம்", @@ -1283,21 +2101,33 @@ "view_album": "ஆல்பத்தைக் காண்க", "view_all": "அனைத்தையும் காண்க", "view_all_users": "அனைத்து பயனர்களையும் காண்க", + "view_details": "விவரங்களைப் பார்", "view_in_timeline": "காலவரிசையில் காண்க", + "view_link": "இணைப்பைக் காண்க", "view_links": "இணைப்புகளைக் காண்க", "view_name": "பார்வை", "view_next_asset": "அடுத்த சொத்தை காண்க", "view_previous_asset": "முந்தைய சொத்தைப் பார்க்கவும்", + "view_qr_code": "QR குறியீட்டைக் காட்டு", + "view_similar_photos": "இதே போன்ற புகைப்படங்களைக் காட்டு", "view_stack": "காண்க அடுக்கு", + "view_user": "பயனரைப் பார்க்கவும்", + "viewer_remove_from_stack": "அடுக்கிலிருந்து அகற்று", + "viewer_stack_use_as_main_asset": "பிரதான சொத்தாகப் பயன்படுத்தவும்", + "viewer_unstack": "அடுக்கை நீக்கு", "visibility_changed": "{எண்ணிக்கை, பன்மை, ஒன்று {# நபர்} மற்ற {# நபர்கள்} க்கு க்கு தெரிவுநிலை மாற்றப்பட்டது", "waiting": "காத்திருக்கிறது", "warning": "எச்சரிக்கை", "week": "வாரம்", "welcome": "வரவேற்கிறோம்", "welcome_to_immich": "இம்மிச்சிற்கு வருக", + "wifi_name": "வைஃபை பெயர்", + "wrong_pin_code": "தவறான பின் குறியீடு", "year": "ஆண்டு", - "years_ago": "{ஆண்டுகள், பன்மை, ஒன்று {# ஆண்டு} மற்ற {# ஆண்டுகள்}}} முன்பு", + "years_ago": "{years, plural, one {# ஆண்டு} other {# ஆண்டுகள்}} முன்பு", "yes": "ஆம்", "you_dont_have_any_shared_links": "உங்களிடம் பகிரப்பட்ட இணைப்புகள் எதுவும் இல்லை", - "zoom_image": "பெரிதாக்க படம்" + "your_wifi_name": "உங்கள் வைஃபை பெயர்", + "zoom_image": "பெரிதாக்க படம்", + "zoom_to_bounds": "எல்லைக்கு பெரிதாக்கு" } diff --git a/i18n/tr.json b/i18n/tr.json index 17296d56f9..38d1a4c227 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -11,16 +11,16 @@ "activity_changed": "Etkinlik {enabled, select, true {etkin} other {devre dışı}}", "add": "Ekle", "add_a_description": "Açıklama ekle", - "add_a_location": "Lokasyon ekle", + "add_a_location": "Bir konum ekle", "add_a_name": "İsim ekle", "add_a_title": "Başlık ekle", "add_birthday": "Doğum günü ekle", "add_endpoint": "Uç nokta ekle", "add_exclusion_pattern": "Hariç tutma deseni ekle", "add_import_path": "İçe aktarma yolu ekle", - "add_location": "Lokasyon ekle", + "add_location": "Konum ekle", "add_more_users": "Daha fazla kullanıcı ekle", - "add_partner": "Partner ekle", + "add_partner": "Ortak ekle", "add_path": "Yol ekle", "add_photos": "Fotoğraf ekle", "add_tag": "Etiket ekle", @@ -28,6 +28,9 @@ "add_to_album": "Albüme ekle", "add_to_album_bottom_sheet_added": "{album} albümüne eklendi", "add_to_album_bottom_sheet_already_exists": "Zaten {album} albümüne ekli", + "add_to_album_toggle": "{album} için seçimi değiştir", + "add_to_albums": "Albümlere ekle", + "add_to_albums_count": "{count} albümlerine ekle", "add_to_shared_album": "Paylaşılan albüme ekle", "add_url": "URL ekle", "added_to_archive": "Arşive eklendi", @@ -35,14 +38,14 @@ "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi", "admin": { "add_exclusion_pattern_description": "Hariç tutma desenleri ekleyin. *, ** ve ? kullanılarak Globbing (temsili yer doldurucu karakter) desteklenir. Farzedelim \"Raw\" adlı bir dizininiz var, içinde ki tüm dosyaları yoksaymak için \"**/Raw/**\" şeklinde yazabilirsiniz. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" yazabilirsiniz. Mutlak yolu yoksaymak için \"/yoksayılacak/olan/yol/**\" şeklinde yazabilirsiniz.", - "admin_user": "Yönetici kullanıcısı", - "asset_offline_description": "Bu harici kütüphane varlığı artık diskte bulunmuyor ve çöp kutusuna taşındı. Dosya kütüphane içinde taşındıysa, yeni karşılık gelen varlık için zaman çizelgenizi kontrol edin. Bu varlığı geri yüklemek için lütfen aşağıdaki dosya yolunun Immich tarafından erişilebilir olduğundan emin olun ve kütüphaneyi tarayın.", + "admin_user": "Yönetici Kullanıcı", + "asset_offline_description": "Bu harici kütüphane öğesi artık diskte bulunmuyor ve çöp kutusuna taşındı. Dosya kütüphane içinde taşındıysa, yeni karşılık gelen öğe için zaman çizelgenizi kontrol edin. Bu öğeyi geri yüklemek için lütfen aşağıdaki dosya yolunun Immich tarafından erişilebilir olduğundan emin olun ve kütüphaneyi tarayın.", "authentication_settings": "Yetkilendirme Ayarları", "authentication_settings_description": "Şifre, OAuth, ve diğer yetkilendirme ayarlarını yönet", "authentication_settings_disable_all": "Tüm giriş yöntemlerini devre dışı bırakmak istediğinize emin misiniz? Giriş yapma fonksiyonu tamamen devre dışı bırakılacak.", "authentication_settings_reenable": "Yeniden aktif etmek için Sunucu Komutu'nu kullanın.", "background_task_job": "Arka Plan Görevleri", - "backup_database": "Veritabanı yığını oluştur", + "backup_database": "Veritabanı Yığını Oluştur", "backup_database_enable_description": "Veritabanı yığınlarını etkinleştir", "backup_keep_last_amount": "Tutulması gereken geçmiş yığını miktarı", "backup_onboarding_1_description": "bulutta veya başka bir fiziksel konumda bulunan yedek kopya.", @@ -52,20 +55,20 @@ "backup_onboarding_footer": "Immich'i yedekleme hakkında daha fazla bilgi için lütfen belgelere bakın.", "backup_onboarding_parts_title": "3-2-1 yedekleme şunları içerir:", "backup_onboarding_title": "Yedeklemeler", - "backup_settings": "Veritabanı yığını ayarları", + "backup_settings": "Veritabanı Yığını Ayarları", "backup_settings_description": "Veritabanı döküm ayarlarını yönet.", "cleared_jobs": "{job} için işler temizlendi", "config_set_by_file": "Ayarlar şuanda config dosyası tarafından ayarlanmıştır", "confirm_delete_library": "{library} kütüphanesini silmek istediğinize emin misiniz?", - "confirm_delete_library_assets": "Bu kütüphaneyi silmek istediğinize emin misiniz? Bu işlem {count, plural, one {# tane varlığı} other {all # tane varlığı}} Immich'den silecek ve bu işlem geri alınamaz. Silinen dosyalar diskten silinmeyecek.", + "confirm_delete_library_assets": "Bu kütüphaneyi silmek istediğinize emin misiniz? Bu işlem {count, plural, one {# tane öğeyi} other {all # tane öğeyi}} Immich'den silecek ve bu işlem geri alınamaz. Dosyalar diskte kalacaktır.", "confirm_email_below": "Onaylamak için aşağıya {email} yazın", "confirm_reprocess_all_faces": "Tüm yüzleri tekrardan işlemek istediğinize emin misiniz? Bu işlem isimlendirilmiş insanları da silecek.", "confirm_user_password_reset": "{user} adlı kullanıcının şifresini sıfırlamak istediğinize emin misiniz?", "confirm_user_pin_code_reset": "{user} adlı kullanıcının PIN kodunu sıfırlamak istediğinize emin misiniz?", "create_job": "Görev oluştur", - "cron_expression": "Cron İfadesi", + "cron_expression": "Cron ifadesi", "cron_expression_description": "Cron formatını kullanarak tarama aralığını belirle. Daha fazla bilgi için örneğin Crontab Guru’ya bakın", - "cron_expression_presets": "Cron İfadesi Önayarları", + "cron_expression_presets": "Cron ifadesi ön ayarları", "disable_login": "Girişi devre dışı bırak", "duplicate_detection_job_description": "Benzer fotoğrafları bulmak için makine öğrenmesini çalıştır. Bu işlem Akıllı Arama'ya bağlıdır", "exclusion_pattern_description": "Kütüphaneyi tararken dosya ve klasörleri görmezden gelmek için dışlama desenlerini kullanabilirsiniz. RAW dosyaları gibi bazı dosya ve klasörleri içe aktarmak istemediğinizde bu seçeneği kullanabilirsiniz.", @@ -74,25 +77,25 @@ "face_detection_description": "Makine öğrenimi kullanarak varlıklardaki yüzleri tespit et. Videolar için sadece küçük resim (thumbnail) dikkate alınır. 'Yenile' tüm varlıkları yeniden işler. 'Sıfırla', mevcut tüm yüz verilerini temizleyerek işlemi yeniden başlatır. 'Eksik' henüz işlenmemiş varlıkları sıraya alır. Tespit edilen yüzler, Yüz Tanıma işlemi tamamlandıktan sonra mevcut ya da yeni kişilere gruplanmak üzere Yüz Tanıma için sıraya alınacaktır.", "facial_recognition_job_description": "Algılanan yüzleri kişilere grupla. Bu adım, Yüz Tespit işlemi tamamlandıktan sonra çalışır. \"Sıfırla\", tüm yüzleri yeniden gruplandırır. \"Eksik\" ise henüz bir kişiye atanmamış yüzleri sıraya alır.", "failed_job_command": "{job} görevi için {command} komutu başarısız", - "force_delete_user_warning": "UYARI: Bu işlem kullanıcıyı ve tüm varlıkları anında kaldıracaktır. Bu geri alınamaz ve dosyalar geri getirilemez.", + "force_delete_user_warning": "UYARI: Bu işlem kullanıcıyı ve tüm öğeleri anında kaldıracaktır. Bu geri alınamaz ve dosyalar geri getirilemez.", "image_format": "Biçim", "image_format_description": "WebP, JPEG'e göre daha küçük dosya boyutu sunar fakat işlemesi daha uzun sürer.", "image_fullsize_description": "Yakınlaştırıldığında kullanılan, meta verileri kaldırılmış tam boyutlu görüntü", "image_fullsize_enabled": "Tam boyutlu görüntü üretimini etkinleştir", "image_fullsize_enabled_description": "Yerleşik önizlemeyi tercih et” seçeneği etkinleştirildiğinde, yerleşik önizlemeler dönüştürme yapılmadan doğrudan kullanılır. JPEG gibi web dostu formatlar bu ayardan etkilenmez.", "image_fullsize_quality_description": "1-100 arasında tam boyutlu görüntü kalitesi. Daha yüksek kalitelidir, ancak daha büyük dosyalar üretir.", - "image_fullsize_title": "Tam boyutlu görüntü ayarları", - "image_prefer_embedded_preview": "Gömülü önizlemeyi tercih et", - "image_prefer_embedded_preview_setting_description": "RAtoğrafları için mümkün olduğunda gömülü önizlemeyi kullan. Bu, bazı fotoğraflarda daha gerçekçi renkler n kameraya bağlıdır ve fotoğrafta normalden daha fazla görüntü bozukluklarına sebep olabilir.", + "image_fullsize_title": "Tam Boyutlu Görüntü Ayarları", + "image_prefer_embedded_preview": "Gömülü ön izlemeyi tercih et", + "image_prefer_embedded_preview_setting_description": "RAW fotoğrafları için mümkün olduğunda gömülü ön izlemeyi kullan. Bu, bazı fotoğraflarda daha gerçekçi renkler n kameraya bağlıdır ve fotoğrafta normalden daha fazla görüntü bozukluklarına sebep olabilir.", "image_prefer_wide_gamut": "Geniş renk aralığını tercih et", "image_prefer_wide_gamut_setting_description": "Önizleme görseli için P3 renk paletini tercih et. Bu, geniş renk paletli fotoğraflarda renk canlılığını daha iyi korur, fakat fotoğraflar eski tarayıcılarda ve eski cihazlarda daha farklı görünebilir. sRGB fotoğraflar renk paletini korumak için sRGB olarak tutulur.", - "image_preview_description": "Orta boyutlu görüntü, meta verisi çıkarılmış, tekil bir varlık görüntülenirken ve makine öğrenimi için kullanılır", + "image_preview_description": "Orta boyutlu görüntü, meta verisi çıkarılmış, tekil bir öğe görüntülenirken ve makine öğrenimi için kullanılır", "image_preview_quality_description": "Ön izleme kalitesi 1-100 arasıdır. Yüksek değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir ve uygulama yanıt verme hızını düşürebilir. Düşük bir değer belirlemek, makine öğrenimi kalitesini etkileyebilir.", - "image_preview_title": "Ön izleme Ayarları", + "image_preview_title": "Ön İzleme Ayarları", "image_quality": "Kalite", "image_resolution": "Çözünürlük", "image_resolution_description": "Daha yüksek çözünürlükle, daha fazla detayı koruyabilir ancak kodlanması daha uzun sürer, daha büyük dosya boyutlarına sahip olur ve uygulamanın yanıt verme hızını azaltabilir.", - "image_settings": "Fotoğraf ayarları", + "image_settings": "Fotoğraf Ayarları", "image_settings_description": "Oluşturulan fotoğrafların kalite ve çözünürlüklerini yönet", "image_thumbnail_description": "Meta verisi çıkarılmış küçük boyutlu küçük resim, ana zaman çizelgesi gibi fotoğraf gruplarını görüntülerken kullanılır", "image_thumbnail_quality_description": "Küçük resim kalitesi 1-100 arasında. Daha yüksek değerler daha iyidir, ancak daha büyük dosyalar üretir ve uygulamanın yanıt hızını azaltabilir.", @@ -102,37 +105,44 @@ "job_not_concurrency_safe": "Bu işlem eşzamanlama için uygun değil.", "job_settings": "Görev Ayarları", "job_settings_description": "Aynı anda çalışacak görevleri yönet", - "job_status": "Görev Statüleri", + "job_status": "Görev Durumu", "jobs_delayed": "{jobCount, plural, other {# gecikmeli}}", "jobs_failed": "{jobCount, plural, other {# Başarısız}}", - "library_created": "{library} kütüphanesi oluşturuldu", + "library_created": "Oluşturulan kütüphane : {library}", "library_deleted": "Kütüphane silindi", "library_import_path_description": "Belirtilecek klasörü içe aktarın. Bu klasör, alt klasörler dahil olmak üzere, görüntüler ve videolar için taranacaktır.", "library_scanning": "Periyodik Tarama", "library_scanning_description": "Periyodik kütüphane taramasını yönet", "library_scanning_enable_description": "Periyodik kütüphane taramasını etkinleştir", - "library_settings": "Harici kütüphane", + "library_settings": "Harici Kütüphane", "library_settings_description": "Harici kütüphane ayarlarını yönet", - "library_tasks_description": "Yeni yada değiştirilmiş varlıklar için dış kütüphaneleri tara", + "library_tasks_description": "Yeni yada değiştirilmiş öğeler için dış kütüphaneleri tara", "library_watching_enable_description": "Harici kütüphanelerdeki dosya değişikliklerini izle", "library_watching_settings": "Kütüphane izleme (DENEYSEL)", "library_watching_settings_description": "Değişen dosyalar için otomatik olarak izle", - "logging_enable_description": "Günlüğü aktifleştir", + "logging_enable_description": "Günlüğü etkinleştir", "logging_level_description": "Etkinleştirildiğinde hangi günlük seviyesi kullanılır.", - "logging_settings": "Günlük tutma", + "logging_settings": "Günlük Tutma", + "machine_learning_availability_checks": "Kullanılabilirlik kontrolleri", + "machine_learning_availability_checks_description": "Kullanılabilir makine öğrenimi sunucularını otomatik olarak algılayın ve tercih edin", + "machine_learning_availability_checks_enabled": "Kullanılabilirlik kontrollerini etkinleştir", + "machine_learning_availability_checks_interval": "Kontrol aralığı", + "machine_learning_availability_checks_interval_description": "Kullanılabilirlik kontrolleri arasındaki milisaniye cinsinden aralık", + "machine_learning_availability_checks_timeout": "İstek zaman aşımı", + "machine_learning_availability_checks_timeout_description": "Kullanılabilirlik kontrolleri için milisaniye cinsinden zaman aşımı", "machine_learning_clip_model": "CLIP modeli", "machine_learning_clip_model_description": "Link burada listelenen CLIP modelinin adı. Bu özelliği değiştirdikten sonra \"Akıllı Arama\" işini tüm fotoğraflar için tekrardan çalıştırmalısınız.", - "machine_learning_duplicate_detection": "Kopya fotoğraf tespiti", + "machine_learning_duplicate_detection": "Kopya Fotoğraf Tespiti", "machine_learning_duplicate_detection_enabled": "Kopya fotoğraf tespitini etkinleştir", - "machine_learning_duplicate_detection_enabled_description": "Devre dışı bırakılırsa aynı ögeler yine de temizlenecek.", + "machine_learning_duplicate_detection_enabled_description": "Devre dışı bırakılırsa aynı öğeler yine de temizlenecek.", "machine_learning_duplicate_detection_setting_description": "Birbirinin kopyası olan varlıkları bulmak için CLIP kullan", "machine_learning_enabled": "Makine öğrenmesini etkinleştir", "machine_learning_enabled_description": "Eğer devre dışı bırakılırsa bütün Makine Öğrenmesi özellikleri devre dışı bırakılacak.", "machine_learning_facial_recognition": "Yüz Tanıma", "machine_learning_facial_recognition_description": "Fotoğraflardaki yüzleri tara, tanı ve gruplandır", - "machine_learning_facial_recognition_model": "Yüz Tanıma Modeli", + "machine_learning_facial_recognition_model": "Yüz tanıma modeli", "machine_learning_facial_recognition_model_description": "Modeller, azalan boyut sırasına göre listelenmiştir. Daha büyük modeller daha yavaştır ve daha fazla bellek kullanır, ancak daha iyi sonuçlar üretir. Bir modeli değiştirdikten sonra tüm görüntüler için yüz algılama işini yeniden çalıştırmanız gerektiğini unutmayın.", - "machine_learning_facial_recognition_setting": "Yüz Tanımayı etkinleştir", + "machine_learning_facial_recognition_setting": "Yüz tanımayı etkinleştir", "machine_learning_facial_recognition_setting_description": "Devre dışı bırakıldığında fotoğraflar yüz tanıma için işlenmeyecek ve Keşfet sayfasındaki Kişiler sekmesini doldurmayacak.", "machine_learning_max_detection_distance": "Maksimum tespit uzaklığı", "machine_learning_max_detection_distance_description": "Resimleri birbirinin çifti saymak için hesap edilecek azami benzerlik ölçüsü, 0.001-0.1 aralığında. Daha yüksek değer daha hassas olup daha fazla çift tespit eder ancak çift olmayan resimleri birbirinin çifti sayabilir.", @@ -167,37 +177,37 @@ "memory_cleanup_job": "Anı temizliği", "memory_generate_job": "Anı oluşturma", "metadata_extraction_job": "Meta verilerinden Ayıkla", - "metadata_extraction_job_description": "GPS ve çözünürlük gibi ger bir varlığın meta veri bilgilerini ayıklayın", + "metadata_extraction_job_description": "GPS, yüzler ve çözünürlük gibi her bir öğeden meta veri bilgilerini çıkarın", "metadata_faces_import_setting": "Yüz içe aktarmayı etkinleştir", "metadata_faces_import_setting_description": "Yüzleri, EXIF verileri ve sidecar dosyalardan getir", "metadata_settings": "Metaveri Ayarları", "metadata_settings_description": "Metaveri ayarlarını yönet", "migration_job": "Birleştirme", - "migration_job_description": "Varlıklar ve yüzler için resim çerçeve önizlemelerini en yeni klasör yapısına aktar", + "migration_job_description": "Öğeler ve yüzler için küçük resimleri en son klasör yapısına taşıyın", "nightly_tasks_cluster_faces_setting_description": "Yeni algılanan yüzlerde yüz tanıma işlemini çalıştırın", "nightly_tasks_cluster_new_faces_setting": "Yeni yüzleri bir araya getirin", "nightly_tasks_database_cleanup_setting": "Veritabanı temizleme görevleri", "nightly_tasks_database_cleanup_setting_description": "Veritabanından eski, süresi dolmuş verileri temizleyin", "nightly_tasks_generate_memories_setting": "Anılar oluşturun", - "nightly_tasks_generate_memories_setting_description": "Varlıklardan yeni anılar yaratın", + "nightly_tasks_generate_memories_setting_description": "Öğelerden yeni anılar yaratın", "nightly_tasks_missing_thumbnails_setting": "Eksik küçük resimleri oluştur", - "nightly_tasks_missing_thumbnails_setting_description": "Küçük resim oluşturmak için küçük resim içermeyen varlıkları sıraya alın", + "nightly_tasks_missing_thumbnails_setting_description": "Küçük resim oluşturmak için küçük resim içermeyen öğeleri sıraya alın", "nightly_tasks_settings": "Gece Görevleri Ayarları", "nightly_tasks_settings_description": "Gece görevlerini yönet", "nightly_tasks_start_time_setting": "Başlangıç saati", "nightly_tasks_start_time_setting_description": "Sunucunun gece görevlerini çalıştırmaya başladığı saat", - "nightly_tasks_sync_quota_usage_setting": "Kota kullanımını senkronize et", + "nightly_tasks_sync_quota_usage_setting": "Kota kullanımını eşzamanla", "nightly_tasks_sync_quota_usage_setting_description": "Mevcut kullanıma göre kullanıcı depolama kotasını güncelle", "no_paths_added": "Yol eklenmedi", "no_pattern_added": "Desen eklenmedi", - "note_apply_storage_label_previous_assets": "Not: Daha önce yüklenen varlıklara Depolama Etiketi uygulamak için şu komutu çalıştırın", + "note_apply_storage_label_previous_assets": "Not: Daha önce yüklenen öğelere Depolama Etiketi uygulamak için şu komutu çalıştırın", "note_cannot_be_changed_later": "NOT: Bu daha sonra değiştirilemez!", "notification_email_from_address": "Şu adresten", "notification_email_from_address_description": "Gönderen e-posta adresi, örneğin: \"Immich Görsel Sunucusu \". E-posta gönderilmesine izin verdiğiniz bir adres kullandığınızdan emin olun.", "notification_email_host_description": "E-posta sunucusunun ana bilgisayarı (örneğin, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Sertifika hatalarını görmezden gel", "notification_email_ignore_certificate_errors_description": "TLS sertifika doğrulama ayarlarını görmezden gel (Önerilmez)", - "notification_email_password_description": "Email sunucusuyla doğrulama için kullanılacak olan şifre", + "notification_email_password_description": "E-posta sunucusunda kimlik doğrulama yaparken kullanılacak şifre", "notification_email_port_description": "Email sunucusunun port numarası (25, 465, 587 gibi)", "notification_email_sent_test_email_button": "Test emaili yolla ve kaydet", "notification_email_setting_description": "Email yollama bildirim ayarları", @@ -206,7 +216,7 @@ "notification_email_test_email_sent": "Test emaili {email} adresine yollandı. Lütfen gelen kutunuzu kontrol edin.", "notification_email_username_description": "Email sunucu doğrulamasında kullanılacak olan kullanıcı adı", "notification_enable_email_notifications": "Email bildirimlerini etkinleştir", - "notification_settings": "Bildirim ayarları", + "notification_settings": "Bildirim Ayarları", "notification_settings_description": "Email ve bildirim ayarlarını yönet", "oauth_auto_launch": "Otomatik başlat", "oauth_auto_launch_description": "Giriş sayfasına girildiğinde OAuth akışını otomatik olarak başlat", @@ -229,16 +239,16 @@ "oauth_storage_quota_claim_description": "Kullanıcıya depolama kotası koymak için kullanılacak değer (en: OAuth claim).", "oauth_storage_quota_default": "Varsayılan depolama kotası (GiB)", "oauth_storage_quota_default_description": "Değer (en: OAuth claim) mevcut değilse GiB cinsinden konulacak kota.", - "oauth_timeout": "İstek zaman aşımı", + "oauth_timeout": "İstek Zaman Aşımı", "oauth_timeout_description": "Milisaniye cinsinden istek zaman aşımı", - "password_enable_description": "Email ve şifre ile giriş yap", - "password_settings": "Şifre giriş", + "password_enable_description": "E-posta ve şifre ile giriş yapın", + "password_settings": "Şifre ile Giriş", "password_settings_description": "Şifre giriş ayarlarını yönet", "paths_validated_successfully": "Tüm yollar başarıyla doğrulandı", "person_cleanup_job": "Kişi temizleme", - "quota_size_gib": "Kota boyutu (GiB)", + "quota_size_gib": "Kota Boyutu (GiB)", "refreshing_all_libraries": "Tüm kütüphaneler yenileniyor", - "registration": "Yönetici kaydı", + "registration": "Yönetici Kaydı", "registration_description": "Sistemdeki ilk kullanıcı olduğunuz için hesabınız Yönetici olarak ayarlandı. Yeni oluşturulan üyeliklerin, ve yönetici görevlerinin sorumlusu olarak atandınız.", "require_password_change_on_login": "Kullanıcının ilk girişinde şifre değiştirmesini zorunlu kıl", "reset_settings_to_default": "Ayarları varsayılana sıfırla", @@ -250,28 +260,28 @@ "server_external_domain_settings_description": "Paylaşılan fotoğraflar için domain, http(s):// dahil", "server_public_users": "Harici Kullanıcılar", "server_public_users_description": "Paylaşılan albümlere bir kullanıcı eklenirken tüm kullanıcılar (ad ve e-posta) listelenir. Devre dışı bırakıldığında, kullanıcı listesi yalnızca yönetici kullanıcılar tarafından kullanılabilir.", - "server_settings": "Sunucu ayarları", + "server_settings": "Sunucu Ayarları", "server_settings_description": "Sunucu ayarlarını yönet", "server_welcome_message": "Hoş geldin mesajı", "server_welcome_message_description": "Giriş sayfasında gösterilen mesaj.", "sidecar_job": "Ek dosya ile taşınan metadata", - "sidecar_job_description": "Ek dosyalardaki metadataları bul ve güncelle", + "sidecar_job_description": "Dosya sisteminden yan araç meta verilerini keşfedin veya eşzamanlayın", "slideshow_duration_description": "Her fotoğrafın kaç saniye görüntüleneceği", - "smart_search_job_description": "Akıllı aramayı desteklemek için tüm varlıklarda makine öğrenmesini çalıştırın", - "storage_template_date_time_description": "Dosyanın yaratılma tarihini, varlığın yaratılma tarihi olarak kullanılacak", + "smart_search_job_description": "Akıllı aramayı desteklemek için tüm öğelerde makine öğrenmesini çalıştırın", + "storage_template_date_time_description": "Öğenin oluşturulma zaman damgası, tarih ve saat bilgisi için kullanılır", "storage_template_date_time_sample": "Örnek tarih {date}", "storage_template_enable_description": "Depolama şablon motorunu etkinleştir", "storage_template_hash_verification_enabled": "Hash doğrulama etkinleştirildi", "storage_template_hash_verification_enabled_description": "Hash doğrulamayı etkinleştirir, eğer ne işe yaradığını bilmiyorsanız bunu devre dışı bırakmayın", "storage_template_migration": "Depolama şablonu birleştirme", - "storage_template_migration_description": "Geçerli {template} ayarlarını daha önce yüklenmiş olan varlıklara uygula", - "storage_template_migration_info": "Depolama şablonu tüm dosya uzantılarını küçük harfe dönüştürecektir. Şablon ayarlarındaki değişiklikler sadece yeni varlıklara uygulanacak. Şablon ayarlarını daha önce yüklenmiş olan varlıklara uygulamak için {job} çalıştırın.", + "storage_template_migration_description": "Geçerli {template} ayarlarını daha önce yüklenmiş olan öğelere uygula", + "storage_template_migration_info": "Depolama şablonu tüm dosya uzantılarını küçük harfe dönüştürecektir. Şablon ayarlarındaki değişiklikler sadece yeni öğelere uygulanacak. Şablon ayarlarını daha önce yüklenmiş olan öğelere uygulamak için {job} çalıştırın.", "storage_template_migration_job": "Depolama Adreslerini Değiştirme Görevi", "storage_template_more_details": "Bu özellik hakkında daha fazla bilgi için, Depolama Şablonu ve onun etkileri kısmına bakın", "storage_template_onboarding_description_v2": "Etkinleştirildiğinde, bu özellik dosyaları kullanıcı tanımlı bir şablona göre otomatik olarak organize eder. Daha fazla bilgi için lütfen belgelere bakın.", "storage_template_path_length": "Tahmini dosya adresi uzunluğu: {length, number}/{limit, number}", "storage_template_settings": "Depolama Şablonu", - "storage_template_settings_description": "Yüklenen dosyanın ismini ve klasör yapısını düzenle", + "storage_template_settings_description": "Yüklenen öğenin ismini ve klasör yapısını düzenle", "storage_template_user_label": "{label} kullanıcını dosyaları için kullanılan alt klasördür", "system_settings": "Sistem Ayarları", "tag_cleanup_job": "Etiket temizleme", @@ -286,14 +296,14 @@ "template_settings_description": "Bildirim şablonlarını yönet", "theme_custom_css_settings": "Özel CSS", "theme_custom_css_settings_description": "CSS (Cascading Style Sheets) kullanılarak Immich'in tasarımı değiştirilebilir.", - "theme_settings": "Tema ayarları", + "theme_settings": "Tema Ayarları", "theme_settings_description": "Immich web arayüzünün özelleştirilmesi ayarlarını yönet", "thumbnail_generation_job": "Önizlemeleri oluştur", - "thumbnail_generation_job_description": "Her kişi ve obje için büyük, küçük ve bulanık thumbnail (küçük resim) oluştur", + "thumbnail_generation_job_description": "Her bir öğe için büyük, küçük ve bulanık küçük resimler ile her kişi için küçük resimler oluşturun", "transcoding_acceleration_api": "Hızlandırma API", "transcoding_acceleration_api_description": "Video formatı çevriminde kullanılacak API. Bu ayara 'mümkün olduğunca' uyulmaktadır; seçilen API'da sorun çıkarsa yazılım tabanlı çevirime dönülür. VP9 donanımınıza bağlı olarak çalışmayabilir.", "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU gerektirir)", - "transcoding_acceleration_qsv": "Quick Sync (7. nesil veya daha yeni bir Intel CPU gerektirir)", + "transcoding_acceleration_qsv": "Hızlı Eşzamanlama (7. nesil veya daha yeni bir Intel CPU gerektirir)", "transcoding_acceleration_rkmpp": "RKMPP (Sadece Rockchip SOC'ler)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Kabul edilen ses kodekleri", @@ -352,15 +362,18 @@ "transcoding_video_codec_description": "VP9 yüksek verimliliğe ve web uyumluluğuna sahiptir, ancak kod dönüştürme işlemi daha uzun sürer. HEVC benzer performans gösterir ancak web uyumluluğu daha düşüktür. H.264 geniş çapta uyumludur ve kod dönüştürmesi hızlıdır, ancak çok daha büyük dosyalar üretir. AV1 en verimli codec'tir ancak eski cihazlarda desteği yoktur.", "trash_enabled_description": "Çöp özelliklerini etkinleştir", "trash_number_of_days": "Gün sayısı", - "trash_number_of_days_description": "Varlıkların kalıcı olarak silinmeden önce çöpte kaç gün tutulacağı", - "trash_settings": "Çöp ayarları", - "trash_settings_description": "Çöp ayarlarını yönet", + "trash_number_of_days_description": "Öğeleri kalıcı olarak silmeden önce çöp kutusunda tutma süresi (gün)", + "trash_settings": "Çöp Kutusu Ayarları", + "trash_settings_description": "Çöp kutusu ayarlarını yönet", + "unlink_all_oauth_accounts": "Tüm OAuth hesaplarının bağlantısını kaldır", + "unlink_all_oauth_accounts_description": "Yeni bir sağlayıcıya geçmeden önce tüm OAuth hesaplarını kaldırılmayı unutmayın.", + "unlink_all_oauth_accounts_prompt": "Tüm OAuth hesaplarını kaldırmak istediğinizden emin misiniz? Bu, her kullanıcı için OAuth kimliğini sıfırlar ve geri alınamaz.", "user_cleanup_job": "Kullanıcı temizleme", - "user_delete_delay": "{user} hesabı ve varlıkları {delay, plural, one {# day} other {# days}} gün içinde kalıcı olarak silinmek için planlandı.", + "user_delete_delay": "{user} hesabı ve öğeleri {delay, plural, one {# day} other {# days}} gün içinde kalıcı olarak silinecektir.", "user_delete_delay_settings": "Silme gecikmesi", - "user_delete_delay_settings_description": "Bir kullanıcının hesabını ve varlıklarını kalıcı olarak silmek için kaldırıldıktan sonra gereken gün sayısı. Kullanıcı silme işi, silinmeye hazır kullanıcıları kontrol etmek için gece yarısı çalışır. Bu ayardaki değişiklikler bir sonraki yürütmede değerlendirilecektir.", - "user_delete_immediately": "{user}'in hesabı ve varlıkları hemen kalıcı olarak silinmek üzere sıraya alınacak.", - "user_delete_immediately_checkbox": "Kullanıcı ve varlıkları hemen silinmek üzere sıraya al", + "user_delete_delay_settings_description": "Bir kullanıcının hesabını ve öğelerini kalıcı olarak silmek için kaldırıldıktan sonra gereken gün sayısı. Kullanıcı silme işi, silinmeye hazır kullanıcıları kontrol etmek için gece yarısı çalışır. Bu ayardaki değişiklikler bir sonraki yürütmede değerlendirilecektir.", + "user_delete_immediately": "{user}'in hesabı ve öğeleri hemen kalıcı olarak silinmek üzere sıraya alınacak.", + "user_delete_immediately_checkbox": "Kullanıcı ve öğeleri hemen silmek için sıraya alın", "user_details": "Kullanıcı Ayrıntıları", "user_management": "Kullanıcı Yönetimi", "user_password_has_been_reset": "Kullanıcının şifresi sıfırlandı:", @@ -368,32 +381,32 @@ "user_restore_description": "{user} kullanıcısı geri yüklenecek.", "user_restore_scheduled_removal": "Kullanıcıyı geri yükle - {date, date, long} tarihinde planlanan kaldırma", "user_settings": "Kullanıcı Ayarları", - "user_settings_description": "Kullanıcı Ayarlarını Yönet", + "user_settings_description": "Kullanıcı ayarlarını yönet", "user_successfully_removed": "Kullanıcı {email} başarıyla kaldırıldı.", "version_check_enabled_description": "Sürüm kontrolü etkin", "version_check_implications": "Sürüm kontrol özelliği, github.com ile periyodik iletişime dayanır", - "version_check_settings": "Versiyon kontrolü", + "version_check_settings": "Sürüm Kontrolü", "version_check_settings_description": "Yeni sürüm bildirimini etkinleştir/devre dışı bırak", "video_conversion_job": "Videoları dönüştür", "video_conversion_job_description": "Tarayıcılar ve cihazlarla daha geniş uyumluluk için videoları dönüştür" }, - "admin_email": "Yönetici Emaili", + "admin_email": "Yönetici E-postası", "admin_password": "Yönetici Şifresi", "administration": "Yönetim", "advanced": "Gelişmiş", - "advanced_settings_beta_timeline_subtitle": "Yeni uygulama deneyimini deneyin", - "advanced_settings_beta_timeline_title": "Beta Zaman Çizelgesi", - "advanced_settings_enable_alternate_media_filter_subtitle": "Eşleme sırasında medyayı alternatif ölçütlere göre süzgeçten geçirmek için bu seçeneği kullanın. Uygulamanın tüm albümleri algılamasında sorun yaşıyorsanız yalnızca bu durumda deneyin.", - "advanced_settings_enable_alternate_media_filter_title": "[DENEYSEL] Alternatif cihaz albüm eşleme süzgeci kullanın", + "advanced_settings_enable_alternate_media_filter_subtitle": "Eşzamanlama sırasında medyayı alternatif ölçütlere göre süzgeçten geçirmek için bu seçeneği kullanın. Uygulamanın tüm albümleri algılamasında sorun yaşıyorsanız yalnızca bu durumda deneyin.", + "advanced_settings_enable_alternate_media_filter_title": "[DENEYSEL] Alternatif cihaz albüm eşzamanlama süzgeci kullanın", "advanced_settings_log_level_title": "Günlük düzeyi: {level}", - "advanced_settings_prefer_remote_subtitle": "Bazı cihazlar yerel varlıklardan küçük resimleri yüklerken çok yavaş çalışır. Bu ayarı etkinleştirerek uzak görüntüleri yükleyin.", + "advanced_settings_prefer_remote_subtitle": "Bazı cihazlar yerel öğelerden küçük resimleri yüklerken çok yavaş çalışır. Bunun yerine uzak görüntüleri yüklemek 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", "advanced_settings_proxy_headers_title": "Proxy Header'lar", + "advanced_settings_readonly_mode_subtitle": "Fotoğrafların yalnızca görüntülenebildiği salt okunur modu etkinleştirir; birden fazla görüntü seçme, paylaşma, aktarma, silme gibi işlemler devre dışı bırakılır. Ana ekrandan kullanıcı avatarı aracılığıyla salt okunur modu Etkinleştirin/Devre dışı bırakın", + "advanced_settings_readonly_mode_title": "Salt okunur Mod", "advanced_settings_self_signed_ssl_subtitle": "Sunucu uç noktası için SSL sertifika doğrulamasını atlar. Kendinden imzalı sertifikalar için gereklidir.", "advanced_settings_self_signed_ssl_title": "Kendi kendine imzalanmış SSL sertifikalarına izin ver", - "advanced_settings_sync_remote_deletions_subtitle": "Web üzerinde işlem yapıldığında, bu aygıttaki varlığı otomatik olarak sil veya geri yükle", - "advanced_settings_sync_remote_deletions_title": "Uzaktan silinmeleri eşle [DENEYSEL]", + "advanced_settings_sync_remote_deletions_subtitle": "Web üzerinde işlem yapıldığında, bu aygıttaki öğeyi otomatik olarak sil veya geri yükle", + "advanced_settings_sync_remote_deletions_title": "Uzaktan silmeleri eşzamanla [DENEYSEL]", "advanced_settings_tile_subtitle": "Gelişmiş kullanıcı ayarları", "advanced_settings_troubleshooting_subtitle": "Sorun giderme için ek özellikleri etkinleştirin", "advanced_settings_troubleshooting_title": "Sorun Giderme", @@ -417,14 +430,15 @@ "album_remove_user_confirmation": "{user} kullanıcısını kaldırmak istediğinize emin misiniz?", "album_search_not_found": "Aramanızla eşleşen albüm bulunamadı", "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_summary": "Albüm özeti", "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_updated_setting_description": "Paylaşılan bir albüme yeni bir öğe eklendiğinde e-posta bildirimi alın", "album_user_left": "{album}den ayrıldınız", "album_user_removed": "{user} kaldırıldı", "album_viewer_appbar_delete_confirm": "Bu albümü hesabınızdan silmek istediğinizden emin misiniz?", "album_viewer_appbar_share_err_delete": "Albüm silinemedi", "album_viewer_appbar_share_err_leave": "Albümden çıkılamadı", - "album_viewer_appbar_share_err_remove": "Albümden öğeleri kaldırmada sorunlar var", + "album_viewer_appbar_share_err_remove": "Albümden öğeler kaldırırken sorunlar yaşanıyor", "album_viewer_appbar_share_err_title": "Albüm başlığı değiştirilemedi", "album_viewer_appbar_share_leave": "Albümden çık", "album_viewer_appbar_share_to": "Paylaşma", @@ -433,8 +447,8 @@ "albums": "Albümler", "albums_count": "{count, plural, one {{count, number} Albüm} other {{count, number} Albüm}}", "albums_default_sort_order": "Varsayılan albüm sıralama düzeni", - "albums_default_sort_order_description": "Yeni albüm oluştururken kullanılacak başlangıç varlık sıralama düzeni.", - "albums_feature_description": "Diğer kullanıcılarla paylaşılabilen varlık koleksiyonları.", + "albums_default_sort_order_description": "Yeni albüm oluştururken kullanılacak başlangıç öğe sıralama düzeni.", + "albums_feature_description": "Diğer kullanıcılarla paylaşılabilen öğe koleksiyonları.", "albums_on_device_count": "Cihazdaki albümler ({count})", "all": "Tümü", "all_albums": "Tüm Albümler", @@ -455,6 +469,7 @@ "app_bar_signout_dialog_title": "Çıkış", "app_settings": "Uygulama Ayarları", "appears_in": "Şurada görünür", + "apply_count": "Uygula ({count, number})", "archive": "Arşiv", "archive_action_prompt": "{count} arşive eklendi", "archive_or_unarchive_photo": "Fotoğrafı arşivle/arşivden çıkar", @@ -470,9 +485,9 @@ "asset_action_share_err_offline": "Çevrimdışı öğeler alınamıyor, atlanıyor", "asset_added_to_album": "Albüme eklendi", "asset_adding_to_album": "Albüme ekleniyor…", - "asset_description_updated": "Varlık açıklaması güncellendi", - "asset_filename_is_offline": "Varlık {filename} çevrimdışı", - "asset_has_unassigned_faces": "Varlık, atanmamış yüzler içeriyor", + "asset_description_updated": "Öğe açıklaması güncellendi", + "asset_filename_is_offline": "Öğe {filename} çevrimdışı", + "asset_has_unassigned_faces": "Öğe, atanmamış yüzler içeriyor", "asset_hashing": "Karma (hashleme) oluşturuluyor…", "asset_list_group_by_sub_title": "Grupla", "asset_list_layout_settings_dynamic_layout_title": "Dinamik düzen", @@ -482,50 +497,58 @@ "asset_list_layout_sub_title": "Düzen", "asset_list_settings_subtitle": "Fotoğraf ızgara düzeni ayarları", "asset_list_settings_title": "Fotoğraf Izgarası", - "asset_offline": "Varlık Çevrim Dışı", - "asset_offline_description": "Bu harici varlık artık diskte bulunmuyor. Yardım için lütfen Immich yöneticinizle iletişime geçin.", + "asset_offline": "Öğe Çevrim Dışı", + "asset_offline_description": "Bu harici öğe artık diskte bulunmuyor. Yardım için lütfen Immich yöneticinizle iletişime geçin.", "asset_restored_successfully": "Öğe başarıyla geri yüklendi", "asset_skipped": "Atlandı", "asset_skipped_in_trash": "Çöpte", + "asset_trashed": "Öğe çöpe atıldı", + "asset_troubleshoot": "Öğe Sorun Giderme", "asset_uploaded": "Yüklendi", "asset_uploading": "Yükleniyor…", "asset_viewer_settings_subtitle": "Galeri görüntüleyici ayarlarını düzenle", "asset_viewer_settings_title": "İçerik Görüntüleyici", - "assets": "Varlıklar", - "assets_added_count": "{count, plural, one {# varlık eklendi} other {# varlık eklendi}}", - "assets_added_to_album_count": "{count, plural, one {# varlık} other {# varlık}} albüme eklendi", - "assets_cannot_be_added_to_album_count": "{count, plural, one {Varlık} other {Varlıklar}} albüme eklenemiyor", - "assets_count": "{count, plural, one {# varlık} other {# varlıklar}}", + "assets": "Öğeler", + "assets_added_count": "Eklendi {count, plural, one {# asset} other {# assets}}", + "assets_added_to_album_count": "Albüme {count, plural, one {# asset} other {# assets}} eklendi", + "assets_added_to_albums_count": "Eklendi {assetTotal, plural, one {# asset} other {# assets}} buraya {albumTotal, plural, one {# album} other {# albums}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} albüme eklenemiyor", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} hiçbir albüme eklenemez", + "assets_count": "{count, plural, one {# öğe} other {# öğeler}}", "assets_deleted_permanently": "{count} öğe kalıcı olarak silindi", "assets_deleted_permanently_from_server": "{count} öğe kalıcı olarak Immich sunucusundan silindi", - "assets_downloaded_failed": "{count, plural, one {# dosya indirildi – {error} dosya indirilemedi} other {# dosya indirildi – {error} dosya indirilemedi}}", + "assets_downloaded_failed": "{count, plural, one {İndirilen # dosya - {error} dosya başarısız} other {İndirilen # dosyalar - {error} dosyalar başarısız oldu}}", "assets_downloaded_successfully": "{count, plural, one {# dosya başarıyla indirildi} other {# dosya başarıyla indirildi}}", - "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_moved_to_trash_count": "{count, plural, one {# öğe} other {# öğeler}} çöpe taşındı", + "assets_permanently_deleted_count": "Kalıcı olarak silindi {count, plural, one {# öğe} other {# öğeler}}", + "assets_removed_count": "Kaldırıldı {count, plural, one {# öğe} other {# öğeler}}", "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": "{count} öğe geri yüklendi", + "assets_restore_confirmation": "Tüm çöp kutusundaki öğeleri geri yüklemek istediğinizden emin misiniz? Bu işlemi geri alamazsınız! Ayrıca, çevrim dışı olan öğelerin bu şekilde geri yüklenemeyeceğini unutmayın.", + "assets_restored_count": "{count, plural, one {# öğe} other {# öğeler}} geri yüklendi", + "assets_restored_successfully": "{count} öğe başarıyla 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": "{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ı", + "assets_trashed_count": "{count, plural, one {# öğe} other {# öğeler}} çöp kutusuna taşındı", + "assets_trashed_from_server": "{count} öğe Immich sunucusundan çöpe atıldı", + "assets_were_part_of_album_count": "{count, plural, one {Öğe zaten} other {Öğeler zaten}} albümün parçasıydı", + "assets_were_part_of_albums_count": "{count, plural, one {Öğe zaten} other {Öğeler zaten}} albümlerin bir 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", "automatic_endpoint_switching_title": "Otomatik URL değiştirme", "autoplay_slideshow": "Otomatik slayt gösterisi", "back": "Geri", "back_close_deselect": "Geri, kapat veya seçimi kaldır", + "background_backup_running_error": "Arka plan yedekleme şu anda çalışıyor, manuel yedekleme başlatılamıyor", "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", + "background_options": "Arka Plan Seçenekleri", "backup": "Yedekle", "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_assets_scatter": "Öğeler 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ç", "backup_album_selection_page_selection_info": "Seçim Bilgileri", "backup_album_selection_page_total_assets": "Toplam eşsiz öğeler", + "backup_albums_sync": "Yedekleme albümlerinin senkronizasyonu", "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…", @@ -580,11 +603,11 @@ "backup_manual_in_progress": "Yükleme halihazırda devam ediyor. Bir süre sonra deneyin", "backup_manual_success": "Başarılı", "backup_manual_title": "Yükleme durumu", + "backup_options": "Yedekleme Seçenekleri", "backup_options_page_title": "Yedekleme seçenekleri", "backup_setting_subtitle": "Arka planda ve ön planda yükleme ayarlarını düzenle", + "backup_settings_subtitle": "Yükleme ayarlarını yönet", "backward": "Geriye doğru", - "beta_sync": "Beta Senkronizasyon Durumu", - "beta_sync_subtitle": "Yeni senkronizasyon sistemini yönetin", "biometric_auth_enabled": "Biyometrik kimlik doğrulama etkin", "biometric_locked_out": "Biyometrik kimlik doğrulaması kilitli", "biometric_no_options": "Biyometrik seçenek yok", @@ -633,31 +656,34 @@ "change_name": "İsim değiştir", "change_name_successfully": "Adı başarıyla değiştirildi", "change_password": "Şifre Değiştir", - "change_password_description": "Bu ya sistemdeki ilk oturum açışınız ya da şifre değişikliği için bir talepte bulunuldu. Lütfen yeni şifreyi aşağıya yazınız.", - "change_password_form_confirm_password": "Parola Onayı", - "change_password_form_description": "Merhaba {name},\n\nBu sisteme ilk kez giriş yaptınız veya parolanızı değiştirmeniz için bir talepte bulunuldu. Lütfen aşağıya yeni parolanızı girin.", - "change_password_form_new_password": "Yeni Parola", - "change_password_form_password_mismatch": "Parolalar eşleşmiyor", - "change_password_form_reenter_new_password": "Tekrar Yeni Parola", + "change_password_description": "Bu sisteme ilk kez giriş yapıyorsunuz veya şifrenizi değiştirmek için bir istekte bulunuldu. Lütfen aşağıya yeni şifrenizi girin.", + "change_password_form_confirm_password": "Şifreyi Onayla", + "change_password_form_description": "Merhaba {name},\n\nBu sisteme ilk kez giriş yapıyorsunuz veya şifrenizi değiştirmek için bir istekte bulunuldu. Lütfen aşağıya yeni şifrenizi girin.", + "change_password_form_new_password": "Yeni Şifre", + "change_password_form_password_mismatch": "Şifreler eşleşmiyor", + "change_password_form_reenter_new_password": "Yeni Şifreyi Tekrar Giriniz", "change_pin_code": "PIN kodunu değiştirin", "change_your_password": "Şifreni değiştir", "changed_visibility_successfully": "Görünürlük başarıyla değiştirildi", - "check_corrupt_asset_backup": "Bozuk yedek dosyalarını kontrol et", + "charging": "Şarj oluyor", + "charging_requirement_mobile_backup": "Arka plan yedekleme için cihazın şarjda olması gerekir", + "check_corrupt_asset_backup": "Bozuk öğe yedeklemelerini kontrol et", "check_corrupt_asset_backup_button": "Kontrol et", - "check_corrupt_asset_backup_description": "Bu kontrolü yalnızca Wi-Fi üzerinden ve tüm dosyalar yedeklendikten sonra çalıştırın. İşlem birkaç dakika sürebilir.", + "check_corrupt_asset_backup_description": "Bu kontrolü yalnızca Wi-Fi üzerinden ve tüm öğeler yedeklendikten sonra çalıştırın. İşlem birkaç dakika sürebilir.", "check_logs": "Günlükleri Kontrol Et", "choose_matching_people_to_merge": "Birleştirmek için eşleşen kişileri seçiniz", "city": "Şehir", - "clear": "Temiz", + "clear": "Temizle", "clear_all": "Hepsini temizle", "clear_all_recent_searches": "Son aramaların hepsini temizle", - "clear_message": "Mesajı Temizle", - "clear_value": "Değeri Temizle", + "clear_file_cache": "Dosya Önbelleği Temizle", + "clear_message": "Mesajı temizle", + "clear_value": "Değeri temizle", "client_cert_dialog_msg_confirm": "Tamam", - "client_cert_enter_password": "Parola Gir", + "client_cert_enter_password": "Şifreyi Girin", "client_cert_import": "İçe Aktar", "client_cert_import_success_msg": "İstemci sertifikası içe aktarıldı", - "client_cert_invalid_msg": "Geçersiz sertifika dosyası veya yanlış parola", + "client_cert_invalid_msg": "Geçersiz sertifika dosyası veya yanlış şifre", "client_cert_remove_msg": "İstemci sertifikası kaldırıldı", "client_cert_subtitle": "Yalnızca PKCS12 (.p12, .pfx) biçimini destekler. Sertifika İçe Aktarma/Kaldırma yalnızca oturum açmadan önce kullanılabilir", "client_cert_title": "SSL İstemci Sertifikası", @@ -676,9 +702,9 @@ "completed": "Tamamlandı", "confirm": "Onayla", "confirm_admin_password": "Yönetici Şifresini Onayla", - "confirm_delete_face": "Varlıktan {name} yüzünü silmek istediğinizden emin misiniz?", + "confirm_delete_face": "Öğeden {name} yüzünü silmek istediğinizden emin misiniz?", "confirm_delete_shared_link": "Bu paylaşılan bağlantıyı silmek istediğinizden emin misiniz?", - "confirm_keep_this_delete_others": "Yığındaki diğer tüm öğeler bu varlık haricinde silinecektir. Devam etmek istediğinizden emin misiniz?", + "confirm_keep_this_delete_others": "Bu öğe hariç, yığındaki diğer tüm öğeler silinecektir. Devam etmek istediğinizden emin misiniz?", "confirm_new_pin_code": "Yeni PIN kodunu onaylayın", "confirm_password": "Şifreyi onayla", "confirm_tag_face": "Bu yüzü {name} olarak etiketlemek ister misiniz?", @@ -703,7 +729,7 @@ "copy_image": "Resmi Kopyala", "copy_link": "Bağlantıyı kopyala", "copy_link_to_clipboard": "Bağlantıyı panoya kopyala", - "copy_password": "Parolayı kopyala", + "copy_password": "Şifreyi kopyala", "copy_to_clipboard": "Panoya Kopyala", "country": "Ülke", "cover": "Kapla", @@ -717,15 +743,17 @@ "create_link_to_share_description": "Bağlantıya sahip olan herkesin seçilen fotoğrafları görmesine izin ver", "create_new": "YENİ OLUŞTUR", "create_new_person": "Yeni kişi oluştur", - "create_new_person_hint": "Seçili varlıkları yeni bir kişiye atayın", + "create_new_person_hint": "Seçili öğeleri yeni bir kişiye atayın", "create_new_user": "Yeni kullanıcı oluştur", - "create_shared_album_page_share_add_assets": "İÇERİK EKLE", + "create_shared_album_page_share_add_assets": "ÖĞELER EKLE", "create_shared_album_page_share_select_photos": "Fotoğrafları Seç", + "create_shared_link": "Paylaşılan bağlantı oluştur", "create_tag": "Etiket oluştur", "create_tag_description": "Yeni bir etiket oluşturun. İç içe geçmiş etiketler için, etiketi tam yolu ve eğik çizgileri de dahil ederek giriniz.", "create_user": "Kullanıcı oluştur", "created": "Oluşturuldu", "created_at": "Oluşturuldu", + "creating_linked_albums": "Bağlantılı albümler oluşturuluyor...", "crop": "Kes", "curated_object_page_title": "Nesneler", "current_device": "Mevcut cihaz", @@ -745,15 +773,16 @@ "date_of_birth_saved": "Doğum günü başarı ile kaydedildi", "date_range": "Tarih aralığı", "day": "Gün", + "days": "Günler", "deduplicate_all": "Tüm kopyaları kaldır", "deduplication_criteria_1": "Resim boyutu (bayt olarak)", "deduplication_criteria_2": "EXIF veri sayısı", "deduplication_info": "Tekilleştirme Bilgileri", - "deduplication_info_description": "Varlıkları otomatik olarak önceden seçmek ve yinelenenleri toplu olarak kaldırmak için şunlara bakıyoruz:", + "deduplication_info_description": "Öğeleri otomatik olarak önceden seçmek ve yinelenenleri toplu olarak kaldırmak için şunlara bakıyoruz:", "default_locale": "Varsayılan Yerel Ayar", "default_locale_description": "Tarihleri ve sayıları tarayıcınızın yerel ayarına göre biçimlendirin", "delete": "Sil", - "delete_action_confirmation_message": "Bu varlığı silmek istediğinizden emin misiniz? Bu işlem, varlığı sunucunun çöp kutusuna taşıyacak ve yerel olarak silmek isteyip istemediğinizi soracaktır", + "delete_action_confirmation_message": "Bu öğeyi silmek istediğinizden emin misiniz? Bu işlem, öğeyi sunucunun çöp kutusuna taşıyacak ve yerel olarak silmek isteyip istemediğinizi soracaktır", "delete_action_prompt": "{count} silindi", "delete_album": "Albümü sil", "delete_api_key_prompt": "Bu API anahtarını silmek istediğinizden emin misiniz?", @@ -774,13 +803,13 @@ "delete_others": "Diğerlerini sil", "delete_permanently": "Kalıcı olarak sil", "delete_permanently_action_prompt": "{count} kalıcı olarak silindi", - "delete_shared_link": "Paylaşılmış linki sil", - "delete_shared_link_dialog_title": "Paylaşılan Bağlantı Sil", + "delete_shared_link": "Paylaşılan bağlantıyı sil", + "delete_shared_link_dialog_title": "Paylaşılan Bağlantıyı Sil", "delete_tag": "Etiketi sil", "delete_tag_confirmation_prompt": "{tagName} etiketini silmek istediğinizden emin misiniz?", "delete_user": "Kullanıcıyı sil", "deleted_shared_link": "Paylaşılan bağlantı silindi", - "deletes_missing_assets": "Diskte eksik olan varlıkları siler", + "deletes_missing_assets": "Diskte eksik olan öğeleri siler", "description": "Açıklama", "description_input_hint_text": "Açıklama ekle...", "description_input_submit_error": "Açıklama güncellenirken hata oluştu, daha fazla ayrıntı için günlüğü kontrol edin", @@ -797,12 +826,12 @@ "display_options": "Görüntüleme seçenekleri", "display_order": "Gösterim sıralaması", "display_original_photos": "Orijinal fotoğrafları göster", - "display_original_photos_setting_description": "Orijinal varlık web uyumlu olduğunda, bir varlığı görüntülerken küçük resimler yerine orijinal fotoğrafı görüntülemeyi tercih edin. Bu, fotoğraf görüntüleme hızlarının yavaşlamasına neden olabilir.", + "display_original_photos_setting_description": "Orijinal öğe web uyumlu olduğunda, bir öğeyi görüntülerken küçük resimler yerine orijinal fotoğrafı görüntülemeyi tercih edin. Bu, fotoğraf görüntüleme hızlarının yavaşlamasına neden olabilir.", "do_not_show_again": "Bu mesajı bir daha gösterme", "documentation": "Dokümantasyon", "done": "Bitti", "download": "İndir", - "download_action_prompt": "{count} varlık indiriliyor", + "download_action_prompt": "{count} öğe indiriliyor", "download_canceled": "İndirme iptal edildi", "download_complete": "İndirme tamamlandı", "download_enqueue": "İndirme sıraya alındı", @@ -814,13 +843,13 @@ "download_notfound": "İndirme bulunamadı", "download_paused": "İndirme duraklatıldı", "download_settings": "İndir", - "download_settings_description": "Varlık indirme ile ilgili ayarları yönetin", + "download_settings_description": "Öğe indirme ile ilgili ayarları yönetin", "download_started": "İndirme başladı", "download_sucess": "İndirme başarılı", "download_sucess_android": "Medya DCIM/Immich klasörüne indirildi", "download_waiting_to_retry": "Yeniden denemek için bekleniyor", "downloading": "İndiriliyor", - "downloading_asset_filename": "Varlık indiriliyor {filename}", + "downloading_asset_filename": "Öğe indiriliyor {filename}", "downloading_media": "Medya indiriliyor", "drop_files_to_upload": "Dosyaları yüklemek için herhangi bir yere bırakın", "duplicates": "Kopyalar", @@ -829,9 +858,12 @@ "edit": "Düzenle", "edit_album": "Albümü düzenle", "edit_avatar": "Avatarı Düzenle", - "edit_birthday": "Doğum Günü Düzenle", + "edit_birthday": "Doğum gününü düzenle", "edit_date": "Tarihi Düzenle", "edit_date_and_time": "Tarih ve zamanı düzenleyin", + "edit_date_and_time_action_prompt": "{count} tarih ve zaman düzenlendi", + "edit_date_and_time_by_offset": "Tarihi ofset ile değiştir", + "edit_date_and_time_by_offset_interval": "Yeni tarih aralığı: {from}'dan {to}'a kadar", "edit_description": "Açıklamayı düzenle", "edit_description_prompt": "Lütfen yeni bir açıklama seçin:", "edit_exclusion_pattern": "Hariç tutma desenini düzenle", @@ -858,7 +890,7 @@ "email_notifications": "E-posta bildirimleri", "empty_folder": "Bu klasör boş", "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!", + "empty_trash_confirmation": "Çöp kutusunu boşaltmak istediğinizden emin misiniz? Bu işlem, çöp kutusundaki tüm varlıkları Immich'ten kalıcı olarak silecektir.\nBu işlemi geri alamazsınız!", "enable": "Etkinleştir", "enable_backup": "Yedeklemeyi Etkinleştir", "enable_biometric_auth_description": "Biyometrik kimlik doğrulamasını etkinleştirmek için PIN kodu girin", @@ -866,54 +898,58 @@ "end_date": "Bitiş tarihi", "enqueued": "Kuyruğa alındı", "enter_wifi_name": "Wi-Fi adını girin", - "enter_your_pin_code": "Pin kodu girin", + "enter_your_pin_code": "PIN kodunuzu girin", "enter_your_pin_code_subtitle": "Kilitli klasöre erişmek için PIN kodunuzu 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_delete_face": "Öğeden yüz silme hatası", + "error_getting_places": "Konum bilgisi alınırken hata oluştu", "error_loading_image": "Resim yüklenirken hata oluştu", + "error_loading_partners": "Ortakları yükleme hatası: {error}", "error_saving_image": "Hata: {error}", "error_tag_face_bounding_box": "Yüz etiketleme hatası – sınırlayıcı kutu koordinatları alınamadı", "error_title": "Bir Hata Oluştu - Bir şeyler ters gitti", "errors": { - "cannot_navigate_next_asset": "Sonraki varlığa geçiş yapılamıyor", - "cannot_navigate_previous_asset": "Önceki varlığa geçiş yapılamıyor", + "cannot_navigate_next_asset": "Sonraki öğeye geçiş yapılamıyor", + "cannot_navigate_previous_asset": "Önceki öğeye geçiş yapılamıyor", "cant_apply_changes": "Değişiklikler uygulanamıyor", "cant_change_activity": "Etkinliği {enabled, select, true {devre dışı bırakamıyor} other {etkinleştiremiyor}}", - "cant_change_asset_favorite": "Varlığın favori durumunu değiştiremiyor", - "cant_change_metadata_assets_count": "{count, plural, one {# varlığın} other {# varlıkların}} meta verisi değiştirilemiyor", + "cant_change_asset_favorite": "Öğenin favori durumu değiştirilemiyor", + "cant_change_metadata_assets_count": "{count, plural, one {# öğenin} other {# öğelerin}} meta verisi değiştirilemiyor", "cant_get_faces": "Yüzler alınamadı", "cant_get_number_of_comments": "Yorumların sayısı alınamadı", "cant_search_people": "Kişiler aranamıyor", "cant_search_places": "Mekanlar aranamıyor", - "error_adding_assets_to_album": "Albüme varlık ekleme hatası", + "error_adding_assets_to_album": "Albüme öğe ekleme hatası", "error_adding_users_to_album": "Albüme kullanıcı ekleme hatası", "error_deleting_shared_user": "Paylaşılan kullanıcı silme hatası", "error_downloading": "{filename} indirme hatası", "error_hiding_buy_button": "Satın alma butonu gizleme hatası", - "error_removing_assets_from_album": "Varlığı albümden silme hatası, daha fazla detay için konsolu kontrol et", - "error_selecting_all_assets": "Bütün varlıkları seçme hatası", + "error_removing_assets_from_album": "Öğeyi albümden silme hatası, daha fazla detay için konsolu kontrol et", + "error_selecting_all_assets": "Tüm öğeleri seçerken hata oluştu", "exclusion_pattern_already_exists": "Bu dışlama modeli halihazırda mevcut.", "failed_to_create_album": "Albüm oluşturulamadı", "failed_to_create_shared_link": "Paylaşılan bağlantı oluşturulamadı", "failed_to_edit_shared_link": "Paylaşılan bağlantı düzenlenemedi", "failed_to_get_people": "Kişiler alınamadı", "failed_to_keep_this_delete_others": "Bu öğenin tutulması ve diğer öğenin silinmesi başarısız oldu", - "failed_to_load_asset": "Varlık yüklenemedi", - "failed_to_load_assets": "Varlıklar yüklenemedi", + "failed_to_load_asset": "Öğe yüklenemedi", + "failed_to_load_assets": "Öğeler yüklenemedi", "failed_to_load_notifications": "Bildirim yüklenemedi", "failed_to_load_people": "Kişiler yüklenemedi", "failed_to_remove_product_key": "Ürün anahtarı kaldırılamadı", - "failed_to_stack_assets": "Varlıklar yığınlanamadı", - "failed_to_unstack_assets": "Varlıkların yığını kaldırılamadı", + "failed_to_reset_pin_code": "PIN kodu sıfırlanamadı", + "failed_to_stack_assets": "Öğeler yığınlanamadı", + "failed_to_unstack_assets": "Öğelerin yığını kaldırılamadı", "failed_to_update_notification_status": "Bildirim durumu güncellenemedi", "import_path_already_exists": "Bu içe aktarma yolu halihazırda mevcut.", "incorrect_email_or_password": "Yanlış e-posta veya şifre", "paths_validation_failed": "{paths, plural, one {# Yol} other {# Yollar}} doğrulanamadı", "profile_picture_transparent_pixels": "Profil resimleri şeffaf piksele sahip olamaz. Lütfen resme yakınlaştırın ve/veya resmi hareket ettirin.", "quota_higher_than_disk_size": "Disk boyutundan daha yüksek bir kota belirlediniz", + "something_went_wrong": "Bir şeyler ters gitti", "unable_to_add_album_users": "Kullanıcılar albüme eklenemiyor", - "unable_to_add_assets_to_shared_link": "Varlıklar paylaşılan bağlantıya eklenemiyor", + "unable_to_add_assets_to_shared_link": "Öğeler paylaşılan bağlantıya eklenemiyor", "unable_to_add_comment": "Yorum eklenemiyor", "unable_to_add_exclusion_pattern": "Hariç tutma modeli eklenemiyor", "unable_to_add_import_path": "İçe aktarma yolu eklenemiyor", @@ -936,8 +972,8 @@ "unable_to_create_library": "Kütüphane oluşturulamıyor", "unable_to_create_user": "Kullanıcı oluşturulamıyor", "unable_to_delete_album": "Albüm silinemiyor", - "unable_to_delete_asset": "Varlık silinemiyor", - "unable_to_delete_assets": "Varlıklar silinemiyor", + "unable_to_delete_asset": "Öğe silinemiyor", + "unable_to_delete_assets": "Öğeler silinemiyor", "unable_to_delete_exclusion_pattern": "Hariç tutma deseni silinemiyor", "unable_to_delete_import_path": "İçe aktarma yolu silinemiyor", "unable_to_delete_shared_link": "Paylaşılan bağlantı silinemiyor", @@ -957,19 +993,19 @@ "unable_to_log_out_device": "Cihazdan çıkış yapılamıyor", "unable_to_login_with_oauth": "OAuth ile giriş yapılamıyor", "unable_to_play_video": "Video oynatılamıyor", - "unable_to_reassign_assets_existing_person": "Varlıklar {name, select, null {mevcut bir kişiye} other {{name}}} yeniden atanamıyor", - "unable_to_reassign_assets_new_person": "Varlıklar yeni bir kişiye yeniden atanamıyor", + "unable_to_reassign_assets_existing_person": "Öğeler {name, select, null {mevcut bir kişiye} other {{name}}} yeniden atanamıyor", + "unable_to_reassign_assets_new_person": "Öğeler yeni bir kişiye yeniden atanamıyor", "unable_to_refresh_user": "Kullanıcı yenilenemiyor", "unable_to_remove_album_users": "Albüm kullanıcıları kaldırılamıyor", "unable_to_remove_api_key": "API anahtarı kaldırılamıyor", - "unable_to_remove_assets_from_shared_link": "Varlıklar paylaşılan bağlantıdan kaldırılamıyor", + "unable_to_remove_assets_from_shared_link": "Öğeler paylaşılan bağlantıdan kaldırılamıyor", "unable_to_remove_library": "Kütüphane kaldırılamadı", "unable_to_remove_partner": "Ortak kaldırılamıyor", "unable_to_remove_reaction": "Reaksiyon kaldırılamıyor", "unable_to_reset_password": "Şifre sıfırlanamıyor", - "unable_to_reset_pin_code": "Pin kodunu sıfırlanamıyor", + "unable_to_reset_pin_code": "PIN kodu sıfırlanamıyor", "unable_to_resolve_duplicate": "Çiftler çözümlenemiyor", - "unable_to_restore_assets": "Varlıklar geri yüklenemiyor", + "unable_to_restore_assets": "Öğeler geri yüklenemiyor", "unable_to_restore_trash": "Çöp geri yüklenemiyor", "unable_to_restore_user": "Kullanıcı geri yüklenemiyor", "unable_to_save_album": "Albüm kaydedilemiyor", @@ -983,7 +1019,7 @@ "unable_to_set_feature_photo": "Özellikli fotoğraf ayarlanamıyor", "unable_to_set_profile_picture": "Profil resmi ayarlanamıyor", "unable_to_submit_job": "Görev gönderilemiyor", - "unable_to_trash_asset": "Varlık çöp kutusuna taşınamıyor", + "unable_to_trash_asset": "Öğe çöp kutusuna taşınamıyor", "unable_to_unlink_account": "Hesap bağlantısı kaldırılamıyor", "unable_to_unlink_motion_video": "Hareket videosunun bağlantısı kaldırılamıyor", "unable_to_update_album_cover": "Albüm resmi güncellenemiyor", @@ -1025,15 +1061,16 @@ "face_unassigned": "Yüz atanmadı", "failed": "Başarısız", "failed_to_authenticate": "Kimlik doğrulaması yapılamadı", - "failed_to_load_assets": "Varlıklar yüklenemedi", + "failed_to_load_assets": "Öğeler yüklenemedi", "failed_to_load_folder": "Klasör yüklenemedi", - "favorite": "Gözde", - "favorite_action_prompt": "{count} gözdelere eklendi", - "favorite_or_unfavorite_photo": "Gözdeye ekle veya çıkar", - "favorites": "Gözdeler", - "favorites_page_no_favorites": "Gözde öge bulunamadı", - "feature_photo_updated": "Özellikli fotoğraf güncellendi", + "favorite": "Favori", + "favorite_action_prompt": "{count} Favorilere eklendi", + "favorite_or_unfavorite_photo": "Favorilere ekle veya çıkar", + "favorites": "Favoriler", + "favorites_page_no_favorites": "Favori öğe bulunamadı", + "feature_photo_updated": "Öne çıkan fotoğraf güncellendi", "features": "Özellikler", + "features_in_development": "Geliştirme Aşamasındaki Özellikler", "features_setting_description": "Uygulamanın özelliklerini yönet", "file_name": "Dosya adı", "file_name_or_extension": "Dosya adı veya uzantı", @@ -1043,21 +1080,26 @@ "filter_people": "Kişileri filtrele", "filter_places": "Yerleri süz", "find_them_fast": "Adlarına göre hızlıca bul", + "first": "İlk", "fix_incorrect_match": "Yanlış eşleştirmeyi düzelt", "folder": "Klasör", "folder_not_found": "Klasör bulunamadı", "folders": "Klasörler", "folders_feature_description": "Dosya sistemindeki fotoğraf ve videoları klasör görünümüyle keşfedin", + "forgot_pin_code_question": "PIN kodunuzu mu unuttunuz?", "forward": "İleri", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Bu özellik, çalışabilmek için Google'dan harici kaynaklar yükler.", "general": "Genel", + "geolocation_instruction_location": "GPS koordinatları olan bir öğeyi tıklayarak konumunu kullanın veya haritadan doğrudan bir konum seçin", "get_help": "Yardım Al", "get_wifiname_error": "Wi-Fi adı alınamadı. Gerekli izinleri verdiğinizden ve bir Wi-Fi ağına bağlı olduğunuzdan emin olun", "getting_started": "Başlarken", "go_back": "Geri git", "go_to_folder": "Klasöre git", "go_to_search": "Aramaya git", + "gps": "GPS", + "gps_missing": "GPS yok", "grant_permission": "İzin ver", "group_albums_by": "Albümleri gruplandır...", "group_country": "Ülkeye göre grupla", @@ -1068,14 +1110,14 @@ "haptic_feedback_switch": "Dokunsal geri bildirimi aç", "haptic_feedback_title": "Dokunsal Geri Bildirim (Haptic Feedback)", "has_quota": "Kota var", - "hash_asset": "Hash varlığı", - "hashed_assets": "Hashlenmiş varlıklar", + "hash_asset": "Karma öğe", + "hashed_assets": "Karma öğeler", "hashing": "Hashleme", "header_settings_add_header_tip": "Header Ekle", "header_settings_field_validator_msg": "Değer boş olamaz", "header_settings_header_name_input": "Header adı", "header_settings_header_value_input": "Header değeri", - "headers_settings_tile_subtitle": "Uygulamanın her ağ isteğiyle birlikte göndermesi gereken proxy header'ları tanımlayın", + "headers_settings_tile_subtitle": "Uygulamanın her ağ isteğinde göndermesi gereken proxy başlıklarını tanımlayın", "headers_settings_tile_title": "Özel proxy headers", "hi_user": "Merhaba {name} {email}", "hide_all_people": "Tüm kişileri gizle", @@ -1084,24 +1126,25 @@ "hide_password": "Şifreyi gizle", "hide_person": "Kişiyi gizle", "hide_unnamed_people": "İsimsiz kişileri gizle", - "home_page_add_to_album_conflicts": "{album} albümüne {added} öğe eklendi. {failed} varlık zaten albümdeydi.", + "home_page_add_to_album_conflicts": "{album} albümüne {added} öğe eklendi. {failed} öğe zaten albümdeydi.", "home_page_add_to_album_err_local": "Yerel öğeler henüz albümlere eklenemiyor, atlanıyor", "home_page_add_to_album_success": "{album} albümüne {added} öğe eklendi.", - "home_page_album_err_partner": "Partner öğeleri henüz bir albüme eklenemiyor, atlanıyor", + "home_page_album_err_partner": "Ortak öğeler henüz bir albüme eklenemiyor, atlanıyor", "home_page_archive_err_local": "Yerel öğeler henüz arşivlenemiyor, atlanıyor", - "home_page_archive_err_partner": "Partner öğeleri henüz arşivlenemiyor, atlanıyor", + "home_page_archive_err_partner": "Ortak öğeler henüz arşivlenemiyor, atlanıyor", "home_page_building_timeline": "Zaman çizelgesi oluşturuluyor", - "home_page_delete_err_partner": "Partner öğeleri silinemez, atlanıyor", + "home_page_delete_err_partner": "Ortak öğeler silinemez, atlanıyor", "home_page_delete_remote_err_local": "Uzaktan silme seçimindeki yerel öğeler atlanıyor", - "home_page_favorite_err_local": "Yerel ögeler henüz gözdelere eklenemiyor, atlanıyor", - "home_page_favorite_err_partner": "Ortak ögeleri henüz gözdelere eklenemiyor, atlanıyor", - "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_locked_error_local": "Yerel varlıklar kilitli klasöre taşınamıyor, atlanıyor", - "home_page_locked_error_partner": "Ortak varlıklar kilitli klasöre taşınamıyor, atlanıyor", + "home_page_favorite_err_local": "Yerel öğeler henüz favorilere eklenemiyor, atlanıyor", + "home_page_favorite_err_partner": "Ortak öğeler henüz favorilere eklenemiyor, atlanıyor", + "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 seçtiğinizden emin olun", + "home_page_locked_error_local": "Yerel öğeler kilitli klasöre taşınamıyor, atlanıyor", + "home_page_locked_error_partner": "Ortak öğeler kilitli klasöre taşınamıyor, atlanıyor", "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": "Ana bilgisayar", "hour": "Saat", + "hours": "Saatler", "id": "ID", "idle": "Boşta", "ignore_icloud_photos": "iCloud Fotoğraflarını Yok Say", @@ -1129,7 +1172,7 @@ "in_archive": "Arşivde", "include_archived": "Arşivlenenleri dahil et", "include_shared_albums": "Paylaşılmış albümleri dahil et", - "include_shared_partner_assets": "Paylaşılan ortak varlıkları dahil et", + "include_shared_partner_assets": "Paylaşılan ortak öğeleri dahil et", "individual_share": "Bireysel paylaşım", "individual_shares": "Kişisel paylaşımlar", "info": "Bilgi", @@ -1144,9 +1187,9 @@ "invite_people": "Kişileri Davet Et", "invite_to_album": "Albüme davet et", "ios_debug_info_fetch_ran_at": "Veri çekme {dateTime} tarihinde çalıştırıldı", - "ios_debug_info_last_sync_at": "Son eşleme: {dateTime}", + "ios_debug_info_last_sync_at": "Son eşzamanlama {dateTime}", "ios_debug_info_no_processes_queued": "Hiçbir arka plan işlemi kuyruğa alınmadı", - "ios_debug_info_no_sync_yet": "Henüz hiçbir arka plan eşleme görevi çalıştırılmadı", + "ios_debug_info_no_sync_yet": "Henüz arka plan eşzamanlama görevi çalıştırılmadı", "ios_debug_info_processes_queued": "{count, plural, one {{count} arka plan işlemi kuyruğa alındı} other {{count} arka plan işlemi kuyruğa alındı}}", "ios_debug_info_processing_ran_at": "İşleme {dateTime} tarihinde çalıştırıldı", "items_count": "{count, plural, one {# Öğe} other {# Öğe}}", @@ -1154,7 +1197,7 @@ "keep": "Koru", "keep_all": "Hepsini koru", "keep_this_delete_others": "Bunu sakla, diğerlerini sil", - "kept_this_deleted_others": "Bu varlık tutuldu ve {count, plural, one {# varlık} other {# varlık}} silindi", + "kept_this_deleted_others": "Bu öğe tutuldu ve {count, plural, one {# varlık} other {# varlık}} silindi", "keyboard_shortcuts": "Klavye kısayolları", "language": "Dil", "language_no_results_subtitle": "Arama teriminizi değiştirmeyi deneyin", @@ -1162,10 +1205,12 @@ "language_search_hint": "Dilleri ara...", "language_setting_description": "Tercih ettiğiniz dili seçiniz", "large_files": "Büyük Dosyalar", + "last": "Son", "last_seen": "Son görülme", - "latest_version": "En son versiyon", + "latest_version": "En Son Sürüm", "latitude": "Enlem", "leave": "Ayrıl", + "leave_album": "Albümden çık", "lens_model": "Mercek modeli", "let_others_respond": "Diğerlerinin yanıt vermesine izin ver", "level": "Seviye", @@ -1179,6 +1224,7 @@ "library_page_sort_title": "Albüm başlığı", "licenses": "Lisanslar", "light": "Açık", + "like": "Beğen", "like_deleted": "Beğeni silindi", "link_motion_video": "Hareket videosunu bağla", "link_to_oauth": "OAuth'a bağla", @@ -1187,9 +1233,10 @@ "loading": "Yükleniyor", "loading_search_results_failed": "Arama sonuçları yüklenemedi", "local": "Yerel", - "local_asset_cast_failed": "Sunucuya yüklenmemiş bir varlık yansıtılamaz", - "local_assets": "Yerel Varlıklar", - "local_network": "Yerel Wi-Fi", + "local_asset_cast_failed": "Sunucuya yüklenmemiş bir öğe yansıtılamaz", + "local_assets": "Yerel Öğeler", + "local_media_summary": "Yerel Medya Özeti", + "local_network": "Yerel ağ", "local_network_sheet_info": "Uygulama belirlenmiş Wi-Fi ağını kullanırken bu URL üzerinden sunucuya bağlanacaktır", "location_permission": "Konum izni", "location_permission_content": "Otomatik geçiş özelliğinin çalışabilmesi için Immich'in mevcut Wi-Fi ağının adını bilmesi, bunu sağlamak için de tam konum iznine ihtiyacı vardır", @@ -1200,6 +1247,7 @@ "location_picker_longitude_hint": "Buraya boylam yazın", "lock": "Kilitle", "locked_folder": "Kilitli Klasör", + "log_detail_title": "Günlük Ayrıntıları", "log_out": "Oturumu kapat", "log_out_all_devices": "Tüm Cihazlarda Oturumu Kapat", "logged_in_as": "{user} olarak oturum açıldı", @@ -1219,17 +1267,18 @@ "login_form_err_trailing_whitespace": "Sondaki boşluk", "login_form_failed_get_oauth_server_config": "OAuth kullanırken bir hata oluştu, sunucu URL'sini kontrol edin", "login_form_failed_get_oauth_server_disable": "OAuth özelliği bu sunucuda mevcut değil", - "login_form_failed_login": "Giriş yaparken hata oluştu, sunucu URL'sini, e-postayı ve parolayı kontrol edin", + "login_form_failed_login": "Giriş yaparken hata oluştu, sunucu URL'sini, e-postayı ve şifreyi kontrol edin", "login_form_handshake_exception": "Sunucuda bir El Sıkışma İstisnası vardı. Kendi kendine imzalanmış bir sertifika kullanıyorsanız, ayarlar menüsünden kendi kendine imzalanmış sertifikalara izin verin.", - "login_form_password_hint": "parola", + "login_form_password_hint": "şifre", "login_form_save_login": "Oturum açık kalsın", - "login_form_server_empty": "Sunucu URL'si girin", + "login_form_server_empty": "Sunucu URL'si girin.", "login_form_server_error": "Sunucuya bağlanılamadı.", "login_has_been_disabled": "Giriş devre dışı bırakıldı.", - "login_password_changed_error": "Parolanız güncellenirken bir hata oluştu.", - "login_password_changed_success": "Parola güncellendi", + "login_password_changed_error": "Şifreniz güncellenirken bir hata oluştu", + "login_password_changed_success": "Şifre başarıyla güncellendi", "logout_all_device_confirmation": "Tüm cihazlarda oturum kapatmak istediğinizden emin misiniz?", "logout_this_device_confirmation": "Bu cihazda oturum kapatmak istediğinizden emin misiniz?", + "logs": "Kayıtlar", "longitude": "Boylam", "look": "Görünüm", "loop_videos": "Videoları döngüye al", @@ -1237,6 +1286,7 @@ "main_branch_warning": "Geliştirme sürümü kullanıyorsunuz. Yayınlanan bir sürüm kullanmanızı önemle tavsiye ederiz!", "main_menu": "Ana menü", "make": "Marka", + "manage_geolocation": "Konumu yönet", "manage_shared_links": "Paylaşılan bağlantıları yönet", "manage_sharing_with_partners": "Ortaklarla paylaşımı yönet", "manage_the_app_settings": "Uygulama ayarlarını yönet", @@ -1245,7 +1295,7 @@ "manage_your_devices": "Cihazlarınızı yönetin", "manage_your_oauth_connection": "OAuth bağlantınızı yönetin", "map": "Harita", - "map_assets_in_bounds": "{count} fotoğraf", + "map_assets_in_bounds": "{count, plural, =0 {Bu alanda fotoğraf yok} one {# photo} other {# photos}}", "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", @@ -1263,14 +1313,15 @@ "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", - "map_settings_only_show_favorites": "Sadece Gözdeleri Göster", + "map_settings_include_show_partners": "Ortakları Dahil Et", + "map_settings_only_show_favorites": "Sadece Favorileri Göster", "map_settings_theme_settings": "Harita Teması", "map_zoom_to_see_photos": "Fotoğrafları görmek için uzaklaştırın", "mark_all_as_read": "Tümünü okundu olarak işaretle", "mark_as_read": "Okundu olarak işaretle", "marked_all_as_read": "Tümü okundu olarak işaretlendi", "matches": "Eşleşenler", + "matching_assets": "Eşleşen Öğeler", "media_type": "Medya türü", "memories": "Anılar", "memories_all_caught_up": "Tümü görüldü", @@ -1289,6 +1340,7 @@ "merged_people_count": "{count, plural, one {# kişi} other {# kişi}} birleştirildi", "minimize": "Küçült", "minute": "Dakika", + "minutes": "Dakikalar", "missing": "Eksik", "model": "Model", "month": "Ay", @@ -1299,8 +1351,8 @@ "move_to_lock_folder_action_prompt": "{count} kilitli klasöre eklendi", "move_to_locked_folder": "Kilitli klasöre taşı", "move_to_locked_folder_confirmation": "Bu fotoğraflar ve videolar tüm albümlerden kaldırılacak ve yalnızca kilitli klasörden görüntülenebilecektir", - "moved_to_archive": "{count, plural, one {# öğe arşive taşındı} other {# öğe arşive taşındı}}", - "moved_to_library": "{count, plural, one {# öğe kitaplığa taşındı} other {# öğe kitaplığa taşındı}}", + "moved_to_archive": "{count, plural, one {# öğe} other {# öğeler}} arşive taşındı", + "moved_to_library": "{count, plural, one {# öğe} other {# öğeler}} kitaplığa taşındı", "moved_to_trash": "Çöp kutusuna taşındı", "multiselect_grid_edit_date_time_err_read_only": "Salt okunur öğelerin tarihi düzenlenemedi, atlanıyor", "multiselect_grid_edit_gps_err_read_only": "Salt okunur öğelerin konumu düzenlenemedi, atlanıyor", @@ -1308,6 +1360,10 @@ "my_albums": "Albümlerim", "name": "İsim", "name_or_nickname": "İsim veya takma isim", + "network_requirement_photos_upload": "Fotoğrafları yedeklemek için mobil veriyi kullan", + "network_requirement_videos_upload": "Videoları yedeklemek için mobil veriyi kullan", + "network_requirements": "Ağ Gereksinimleri", + "network_requirements_updated": "Ağ durumu değişti, yedekleme kuyruğu sıfırlandı", "networking_settings": "Ağ Ayarları", "networking_subtitle": "Sunucu uç nokta ayarlarını düzenle", "never": "Asla", @@ -1317,8 +1373,9 @@ "new_person": "Yeni kişi", "new_pin_code": "Yeni PIN kodu", "new_pin_code_subtitle": "Kilitli klasöre ilk kez erişiyorsunuz. Bu sayfaya güvenli erişim için bir PIN kodu oluşturun", + "new_timeline": "Yeni Zaman Çizelgesi", "new_user_created": "Yeni kullanıcı oluşturuldu", - "new_version_available": "YENİ VERSİYON MEVCUT", + "new_version_available": "YENİ SÜRÜM MEVCUT", "newest_first": "Önce en yeniler", "next": "Sonraki", "next_memory": "Sonraki anı", @@ -1330,23 +1387,28 @@ "no_assets_message": "İLK FOTOĞRAFINIZI YÜKLEMEK İÇİN TIKLAYIN", "no_assets_to_show": "Gösterilecek öğe yok", "no_cast_devices_found": "Yansıtılacak cihaz bulunamadı", + "no_checksum_local": "Sağlama toplamı mevcut değil - yerel varlıkları alamıyor", + "no_checksum_remote": "Sağlama toplamı mevcut değil - uzak varlık alınamıyor", "no_duplicates_found": "Çift bulunamadı.", "no_exif_info_available": "EXIF bilgisi mevcut değil", "no_explore_results_message": "Koleksiyonunuzu keşfetmek için daha fazla fotoğraf yükleyin.", - "no_favorites_message": "En sevdiğiniz fotoğraf ve videoları hızlıca bulmak için gözdelere ekleyin", + "no_favorites_message": "En sevdiğiniz fotoğraf ve videoları hızlıca bulmak için favorilere ekleyin", "no_libraries_message": "Fotoğraf ve videolarınızı görmek için bir harici kütüphane oluşturun", + "no_local_assets_found": "Bu sağlama toplamı ile yerel varlık bulunamadı", "no_locked_photos_message": "Kilitli klasördeki fotoğraf ve videolar gizlidir; kitaplığınızda gezinirken veya arama yaparken görünmezler.", "no_name": "İsim yok", "no_notifications": "Bildirim yok", "no_people_found": "Eşleşen kişi bulunamadı", "no_places": "Yer yok", + "no_remote_assets_found": "Bu sağlama toplamı ile uzaktaki varlık bulunamadı", "no_results": "Sonuç bulunamadı", "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", "no_uploads_in_progress": "Yükleme işlemi yok", + "not_available": "YOK", "not_in_any_album": "Hiçbir albümde değil", "not_selected": "Seçilmedi", - "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", + "note_apply_storage_label_to_previously_uploaded assets": "Not: Daha önce yüklenen öğeler için bir depolama yolu etiketi uygulamak üzere şunu başlatın", "notes": "Notlar", "nothing_here_yet": "Burada henüz bir şey yok", "notification_permission_dialog_content": "Bildirimleri etkinleştirmek için cihaz ayarlarına gidin ve izin verin.", @@ -1359,6 +1421,7 @@ "oauth": "OAuth", "official_immich_resources": "Resmi Immich Kaynakları", "offline": "Çevrim dışı", + "offset": "Ofset", "ok": "Tamam", "oldest_first": "Eski olan önce", "on_this_device": "Bu cihazda", @@ -1370,13 +1433,15 @@ "onboarding_user_welcome_description": "Haydi başlayalım!", "onboarding_welcome_user": "Hoş geldin, {user}", "online": "Çevrimiçi", - "only_favorites": "Sadece gözdeler", + "only_favorites": "Sadece favoriler", "open": "Aç", "open_in_map_view": "Harita görünümünde aç", "open_in_openstreetmap": "OpenStreetMap'te Aç", "open_the_search_filters": "Arama filtrelerini aç", "options": "Seçenekler", "or": "veya", + "organize_into_albums": "Albümler halinde düzenle", + "organize_into_albums_description": "Mevcut eşzamanlama ayarlarını kullanarak mevcut fotoğrafları albümlere ekleyin", "organize_your_library": "Kütüphanenizi düzenleyin", "original": "orijinal", "other": "Diğer", @@ -1391,17 +1456,17 @@ "partner_can_access_location": "Fotoğraf ve videolarınızın çekildiği konum", "partner_list_user_photos": "{user} fotoğrafları", "partner_list_view_all": "Tümünü gör", - "partner_page_empty_message": "Fotoğraflarınız henüz hiçbir partnerle paylaşılmadı.", + "partner_page_empty_message": "Fotoğraflarınız henüz hiçbir ortakla paylaşılmadı.", "partner_page_no_more_users": "Eklenecek başka kullanıcı yok", - "partner_page_partner_add_failed": "Partner eklenemedi", - "partner_page_select_partner": "Partner seç", + "partner_page_partner_add_failed": "Ortak eklenemedi", + "partner_page_select_partner": "Ortak seç", "partner_page_shared_to_title": "Paylaşıldı", "partner_page_stop_sharing_content": "{partner} artık fotoğraflarınıza erişemeyecek.", - "partner_sharing": "Ortak paylaşımı", + "partner_sharing": "Ortak Paylaşımı", "partners": "Ortaklar", "password": "Şifre", - "password_does_not_match": "Şifreler eşleşmiyor", - "password_required": "Şifre gereklidir", + "password_does_not_match": "Şifre eşleşmiyor", + "password_required": "Şifre Gerekiyor", "password_reset_success": "Şifre başarıyla sıfırlandı", "past_durations": { "days": "{days, plural, one {Dün} other {Son # gün}}", @@ -1421,10 +1486,10 @@ "permanent_deletion_warning": "Kalıcı silme uyarısı", "permanent_deletion_warning_setting_description": "Nesneleri kalıcı olarak silerken uyarı göster", "permanently_delete": "Kalıcı olarak sil", - "permanently_delete_assets_count": "{count, plural, one {Dosya} other {Dosyalar}} kalıcı olarak silindi", - "permanently_delete_assets_prompt": "Bu {count, plural, one {dosyayı} other {# dosyaları}} kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem {count, plural, one {bu dosyayı} other {bu dosyaları}} albümlerinizden de kaldırır.", - "permanently_deleted_asset": "Kalıcı olarak silinmiş ögeler", - "permanently_deleted_assets_count": "{count, plural, one {# dosya} other {# dosya}} kalıcı olarak silindi", + "permanently_delete_assets_count": "{count, plural, one {öğe} other {öğeler}} kalıcı olarak silindi", + "permanently_delete_assets_prompt": "Bu {count, plural, one {öğeyi} other {# öğeleri}} kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem {count, plural, one {bu öğeyi} other {bu öğeleri}} albümlerinizden de kaldırır.", + "permanently_deleted_asset": "Kalıcı olarak silinmiş öğeler", + "permanently_deleted_assets_count": "{count, plural, one {# öğe} other {# öğeler}} kalıcı olarak silindi", "permission": "İzin", "permission_empty": "İzniniz boş olmamalı", "permission_onboarding_back": "Geri", @@ -1436,6 +1501,9 @@ "permission_onboarding_permission_limited": "Sınırlı izin. Immich'in tüm fotoğrav ve videolarınızı yedeklemesine ve yönetmesine izin vermek için Ayarlar'da fotoğraf ve video izinlerini verin.", "permission_onboarding_request": "Immich'in fotoğraflarınızı ve videolarınızı görüntüleyebilmesi için izne ihtiyacı var.", "person": "Kişi", + "person_age_months": "{months, plural, one {# month} other {# months}} eski", + "person_age_year_months": "1 yıl, {months, plural, one {# month} other {# months}} eski", + "person_age_years": "{years, plural, other {# sene}} önce", "person_birthdate": "{date} tarihinde doğdu", "person_hidden": "{name}{hidden, select, true { (gizli)} other {}}", "photo_shared_all_users": "Fotoğraflarınızı tüm kullanıcılarla paylaştınız gibi görünüyor veya paylaşacak kullanıcı bulunmuyor.", @@ -1453,12 +1521,13 @@ "places_count": "{count, plural, one {{count, number} yer} other {{count, number} yer}}", "play": "Oynat", "play_memories": "Anıları oynat", - "play_motion_photo": "Hareketli fotoğrafı oynat", + "play_motion_photo": "Hareketli Fotoğrafı Oynat", "play_or_pause_video": "Videoyu oynat ya da durdur", "please_auth_to_access": "Erişim için lütfen kimliğinizi doğrulayın", "port": "Port", "preferences_settings_subtitle": "Uygulama tercihlerini düzenle", "preferences_settings_title": "Tercihler", + "preparing": "Hazırlanıyor", "preset": "Ön ayar", "preview": "Önizleme", "previous": "Önceki", @@ -1475,6 +1544,7 @@ "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_readonly_mode": "Salt okunur mod etkinleştirildi. Çıkmak için kullanıcı avatar simgesine uzun basın.", "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", @@ -1513,6 +1583,7 @@ "purchase_server_description_2": "Destekçi statüsü", "purchase_server_title": "Sunucu", "purchase_settings_server_activated": "Sunucu ürün anahtarı, yönetici tarafından yönetilir", + "query_asset_id": "Öğe Kimliği Sorgulama", "queue_status": "Sırada {count}/{total}", "rating": "Derecelendirme", "rating_clear": "Derecelendirmeyi temizle", @@ -1520,10 +1591,13 @@ "rating_description": "EXIF derecelendirmesini bilgi panelinde göster", "reaction_options": "Tepki seçenekleri", "read_changelog": "Değişiklik günlüğünü oku", + "readonly_mode_disabled": "Salt okunur mod devre dışı", + "readonly_mode_enabled": "Salt okunur mod etkin", + "ready_for_upload": "Yüklemeye hazır", "reassign": "Yeniden ata", - "reassigned_assets_to_existing_person": "{count, plural, one {# dosya} other {# dosya}} {name, select, null {mevcut bir kişiye} other {{name}}} atandı", - "reassigned_assets_to_new_person": "{count, plural, one {# dosya} other {# dosya}} yeni bir kişiye atandı", - "reassing_hint": "Seçili dosyaları mevcut bir kişiye atayın", + "reassigned_assets_to_existing_person": "{count, plural, one {# öğe} other {# öğeler}} {name, select, null {mevcut bir kişiye} other {{name}}} atandı", + "reassigned_assets_to_new_person": "{count, plural, one {# öğe} other {# öğeler}} yeni bir kişiye atandı", + "reassing_hint": "Seçili öğeleri mevcut bir kişiye atayın", "recent": "Son", "recent-albums": "Son kaydedilen albümler", "recent_searches": "Son aramalar", @@ -1543,16 +1617,17 @@ "refreshing_metadata": "Meta veriler yenileniyor", "regenerating_thumbnails": "Küçük resimler yeniden oluşturuluyor", "remote": "Uzaktan", - "remote_assets": "Uzak Varlıklar", + "remote_assets": "Uzak Öğeler", + "remote_media_summary": "Uzaktan Medya Özeti", "remove": "Kaldır", - "remove_assets_album_confirmation": "{count, plural, one {# dosyayı} other {# dosyayı}} albümden çıkarmak istediğinizden emin misiniz?", - "remove_assets_shared_link_confirmation": "{count, plural, one {# dosyayı} other {# dosyayı}} bu paylaşılan bağlantıdan çıkarmak istediğinizden emin misiniz?", - "remove_assets_title": "Dosyaları çıkar?", + "remove_assets_album_confirmation": "{count, plural, one {# öğeyi} other {# öğeleri}} albümden çıkarmak istediğinizden emin misiniz?", + "remove_assets_shared_link_confirmation": "{count, plural, one {# öğeyi} other {# öğeleri}} bu paylaşılan bağlantıdan çıkarmak istediğinizden emin misiniz?", + "remove_assets_title": "Öğeleri çıkar?", "remove_custom_date_range": "Özel tarih aralığını kaldır", - "remove_deleted_assets": "Çevrimdışı dosyaları kaldır", + "remove_deleted_assets": "Silinen Öğeleri Kaldır", "remove_from_album": "Albümden çıkar", "remove_from_album_action_prompt": "{count} albümden kaldırıldı", - "remove_from_favorites": "Gözdelerden çıkar", + "remove_from_favorites": "Favorilerden çıkar", "remove_from_lock_folder_action_prompt": "{count} kilitli klasörden kaldırıldı", "remove_from_locked_folder": "Kilitli klasörden kaldır", "remove_from_locked_folder_confirmation": "Bu fotoğraf ve videoları kilitli klasörden çıkarmak istediğinizden emin misiniz? Çıkarıldıklarında kitaplığınızda görünür olacaklar.", @@ -1564,25 +1639,28 @@ "remove_user": "Kullanıcıyı çıkar", "removed_api_key": "API anahtarı {name} kaldırıldı", "removed_from_archive": "Arşivden çıkarıldı", - "removed_from_favorites": "Gözdelerden kaldırıldı", - "removed_from_favorites_count": "{count, plural, other {#}} gözdelerden çıkarıldı", + "removed_from_favorites": "Favorilerden kaldırıldı", + "removed_from_favorites_count": "{count, plural, other {#}} favorilerden çıkarıldı", "removed_memory": "Anı kaldırıldı", "removed_photo_from_memory": "Fotoğraf anıdan kaldırıldı", - "removed_tagged_assets": "{count, plural, one {# dosya} other {# dosya}} etiketleri kaldırıldı", + "removed_tagged_assets": "{count, plural, one {# öğenin} other {# öğelerin}} etiketleri kaldırıldı", "rename": "Yeniden adlandır", "repair": "Onar", "repair_no_results_message": "Bulunamayan ve eksik dosyalar burada listelenecektir", "replace_with_upload": "Yükleme ile değiştir", "repository": "Depo", - "require_password": "Şifre gerekli", + "require_password": "Şifre gerekiyor", "require_user_to_change_password_on_first_login": "Kullanıcı ilk girişte şifreyi değiştirmeli", "rescan": "Yeniden tara", "reset": "Sıfırla", "reset_password": "Şifreyi sıfırla", "reset_people_visibility": "Kişilerin görünürlüğünü sıfırla", "reset_pin_code": "PIN kodunu sıfırlayın", + "reset_pin_code_description": "PIN kodunuzu unuttuysanız, sıfırlamak için sunucu yöneticisiyle iletişime geçebilirsiniz", + "reset_pin_code_success": "PIN kodu başarıyla sıfırlandı", + "reset_pin_code_with_password": "PIN kodunuzu her zaman şifrenizle sıfırlayabilirsiniz", "reset_sqlite": "SQLite Veritabanını Sıfırla", - "reset_sqlite_confirmation": "SQLite veritabanını sıfırlamak istediğinizden emin misiniz? Verileri yeniden senkronize etmek için oturumu kapatıp tekrar oturum açmanız gerekecektir", + "reset_sqlite_confirmation": "SQLite veritabanını sıfırlamak istediğinizden emin misiniz? Verileri yeniden eşzamanlamak için oturumu kapatıp tekrar oturum açmanız gerekecektir", "reset_sqlite_success": "SQLite veritabanını başarıyla sıfırladınız", "reset_to_default": "Varsayılana sıfırla", "resolve_duplicates": "Çiftleri çöz", @@ -1591,8 +1669,9 @@ "restore_all": "Tümünü geri yükle", "restore_trash_action_prompt": "{count} çöp kutusundan geri yüklendi", "restore_user": "Kullanıcıyı geri yükle", - "restored_asset": "Dosya geri yüklendi", + "restored_asset": "Öğe geri yüklendi", "resume": "Devam et", + "resume_paused_jobs": "Sürdür {count, plural, one {# duraklatılmış iş} other {# duraklatılmış işler}}", "retry_upload": "Yeniden yüklemeyi dene", "review_duplicates": "Çiftleri gözden geçir", "review_large_files": "Büyük dosyaları inceleyin", @@ -1659,7 +1738,7 @@ "search_result_page_new_search_hint": "Yeni Arama", "search_settings": "Ayarları ara", "search_state": "Eyalet/İl ara...", - "search_suggestion_list_smart_search_hint_1": "Akıllı arama varsayılan olarak etkindir, meta verileri aramak için syntax kullanın", + "search_suggestion_list_smart_search_hint_1": "Akıllı arama varsayılan olarak etkindir, meta verileri aramak için şu sözdizimini kullanın ", "search_suggestion_list_smart_search_hint_2": "m:meta-veri-araması", "search_tags": "Etiketleri ara...", "search_timezone": "Saat dilimi ara...", @@ -1686,6 +1765,7 @@ "select_user_for_sharing_page_err_album": "Albüm oluşturulamadı", "selected": "Seçildi", "selected_count": "{count, plural, other {# seçildi}}", + "selected_gps_coordinates": "Seçilen GPS Koordinatları", "send_message": "Mesaj gönder", "send_welcome_email": "Hoş geldin e-postası gönder", "server_endpoint": "Sunucu Uç Noktası", @@ -1695,7 +1775,7 @@ "server_online": "Sunucu çevrimiçi", "server_privacy": "Sunucu Gizliliği", "server_stats": "Sunucu istatistikleri", - "server_version": "Sunucu versiyonu", + "server_version": "Sunucu Sürümü", "set": "Ayarla", "set_as_album_cover": "Albüm resmi olarak ayarla", "set_as_featured_photo": "Öne çıkan fotoğraf olarak ayarla", @@ -1703,9 +1783,9 @@ "set_date_of_birth": "Doğum tarihini ayarla", "set_profile_picture": "Profil resmini ayarla", "set_slideshow_to_fullscreen": "Slayt gösterisini tam ekran yap", - "set_stack_primary_asset": "Birincil varlık olarak ayarla", + "set_stack_primary_asset": "Birincil öğe olarak ayarla", "setting_image_viewer_help": "Görüntüleyici önce küçük resmi gösterir, ardından orta boy önizlemeyi (etkinleştirilmişse) ve son olarak orijinali (etkinleştirilmişse) gösterir.", - "setting_image_viewer_original_subtitle": "Orijinal tam çözünürlüklü görüntüyü göstermek için etkinleştirin. Veri kullanımını azaltmak için devre dışı bırakın (hem ağ hem de cihaz önbelleği).", + "setting_image_viewer_original_subtitle": "Orijinal tam çözünürlüklü görüntüyü (büyük!) yüklemek için etkinleştirin. Veri kullanımını azaltmak için devre dışı bırakın (hem ağ hem de cihaz önbelleği).", "setting_image_viewer_original_title": "Orijinal görüntüyü göster", "setting_image_viewer_preview_subtitle": "Orta çözünürlüklü bir görüntü göstermek için etkinleştirin. Orijinali doğrudan göstermek veya yalnızca küçük resmi kullanmak için devre dışı bırakın.", "setting_image_viewer_preview_title": "Önizleme görüntüsü göster", @@ -1731,7 +1811,7 @@ "settings_saved": "Ayarlar kaydedildi", "setup_pin_code": "PIN kodunu ayarlayın", "share": "Paylaş", - "share_action_prompt": "Paylaşılan {count} varlık", + "share_action_prompt": "Paylaşılan {count} öğe", "share_add_photos": "Fotoğraf ekle", "share_assets_selected": "{count} seçili", "share_dialog_preparing": "Hazırlanıyor...", @@ -1751,7 +1831,7 @@ "shared_intent_upload_button_progress_text": "{current} / {total} Yüklendi", "shared_link_app_bar_title": "Paylaşılan Bağlantılar", "shared_link_clipboard_copied_massage": "Panoya kopyalandı", - "shared_link_clipboard_text": "Bağlantı: {link}\nParola: {password}", + "shared_link_clipboard_text": "Bağlantı: {link}\nŞifre: {password}", "shared_link_create_error": "Paylaşım bağlantısı oluşturulurken hata oluştu", "shared_link_custom_url_description": "Özel bir URL ile bu paylaşılan bağlantıya erişin", "shared_link_edit_description_hint": "Açıklama yazın", @@ -1763,7 +1843,7 @@ "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_password_hint": "Paylaşım şifresini 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 {count} gün içinde doluyor", @@ -1779,10 +1859,10 @@ "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_link_password_description": "Bu paylaşılan bağlantıya erişmek için şifre gerektirir", + "shared_link_password_description": "Bu paylaşılan bağlantıya erişmek için şifre gereklidir", "shared_links": "Paylaşılan bağlantılar", "shared_links_description": "Fotoğraf ve videoları bir bağlantı ile paylaş", - "shared_photos_and_videos_count": "{assetCount, plural, one {# paylaşılan fotoğraf veya video.} other {# paylaşılan fotoğraf & video.}}", + "shared_photos_and_videos_count": "{assetCount, plural, other {# paylaşılan fotoğraflar & videolar.}}", "shared_with_me": "Benimle paylaşılanlar", "shared_with_partner": "{partner} ile paylaşıldı", "sharing": "Paylaşılıyor", @@ -1792,8 +1872,8 @@ "sharing_page_empty_list": "LİSTEYİ BOŞALT", "sharing_sidebar_description": "Yan panelde paylaşılanlara kısa yol göster", "sharing_silver_appbar_create_shared_album": "Yeni paylaşılan albüm", - "sharing_silver_appbar_share_partner": "Partnerle paylaş", - "shift_to_permanent_delete": "Dosyayı kalıcı olarak silmek için ⇧ tuşuna basın", + "sharing_silver_appbar_share_partner": "Ortakla paylaş", + "shift_to_permanent_delete": "Öğeyi kalıcı olarak silmek için ⇧ tuşuna basın", "show_album_options": "Albüm ayarlarını göster", "show_albums": "Albümleri göster", "show_all_people": "Tüm kişileri göster", @@ -1814,6 +1894,7 @@ "show_slideshow_transition": "Slayt geçişini göster", "show_supporter_badge": "Destekçi rozeti", "show_supporter_badge_description": "Destekçi rozetini göster", + "show_text_search_menu": "Metin arama menüsünü göster", "shuffle": "Karıştır", "sidebar": "Yan panel", "sidebar_display_description": "Yan panelde görünüme kısa yol göster", @@ -1829,6 +1910,7 @@ "sort_created": "Oluşturulma tarihi", "sort_items": "Öğe sayısı", "sort_modified": "Değişiklik tarihi", + "sort_newest": "En yeni fotoğraf", "sort_oldest": "En eski fotoğraf", "sort_people_by_similarity": "İnsanları benzerliğe göre sırala", "sort_recent": "En yeni fotoğraf", @@ -1839,10 +1921,11 @@ "stack_duplicates": "Çiftleri yığınla", "stack_select_one_photo": "Yığın için ana fotoğrafı seç", "stack_selected_photos": "Seçili fotoğrafları yığınla", - "stacked_assets_count": "{count, plural, one {# dosya} other {# dosya}} yığınlandı", + "stacked_assets_count": "{count, plural, one {# öğe} other {# öğeler}} yığınlandı", "stacktrace": "Yığın izi", "start": "Başlat", "start_date": "Başlangıç tarihi", + "start_date_before_end_date": "Başlangıç tarihi bitiş tarihinden önce olmalıdır", "state": "Eyalet/İl", "status": "Durum", "stop_casting": "Yansıtmayı durdur", @@ -1862,27 +1945,29 @@ "support_and_feedback": "Destek & Geri Bildirim", "support_third_party_description": "Immich kurulumu üçüncü bir tarafça yapıldı. Yaşadığınız sorunlar bu paketle ilgili olabilir. Lütfen öncelikli olarak aşağıdaki bağlantıları kullanarak bu sağlayıcıyla iletişime geçin.", "swap_merge_direction": "Birleştirme yönünü değiştir", - "sync": "Senkronize et", + "sync": "Eşzamanla", "sync_albums": "Albümleri eşzamanla", "sync_albums_manual_subtitle": "Yüklenmiş fotoğraf ve videoları yedekleme için seçili albümler ile eşzamanlayın", - "sync_local": "Yerel Senkronizasyon", - "sync_remote": "Uzaktan Senkronizasyon", - "sync_upload_album_setting_subtitle": "Seçili albümleri Immich'te oluşturun ve içindekileri Immich'e yükleyin.", + "sync_local": "Yerel Eşzamanlama", + "sync_remote": "Uzaktan Eşzamanlama", + "sync_status": "Eşzamanlama Durumu", + "sync_status_subtitle": "Eşzamanlama sistemini görüntüleyin ve yönetin", + "sync_upload_album_setting_subtitle": "Fotoğraflarınızı ve videolarınızı oluşturun ve Immich'te seçtiğiniz albümlere yükleyin", "tag": "Etiket", - "tag_assets": "Dosyaları etiketle", + "tag_assets": "Öğeleri etiketle", "tag_created": "Etiket oluşturuldu: {tag}", "tag_feature_description": "Etiket temalarına göre gruplandırılmış fotoğraf ve videoları keşfedin", "tag_not_found_question": "Etiket bulunamadı mı? Yeni bir etiket oluşturun.", "tag_people": "İnsanları etiketle", "tag_updated": "Etiket güncellendi: {tag}", - "tagged_assets": "{count, plural, one {# dosya} other {# dosya}} etiketlendi", + "tagged_assets": "{count, plural, one {# öğe} other {# öğeler}} etiketlendi", "tags": "Etiketler", "tap_to_run_job": "Başlatmak için dokunun", "template": "Şablon", "theme": "Tema", "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_storage_indicator_title": "Öğe kutucuklarında depolama göstergesini göster", "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", @@ -1893,7 +1978,7 @@ "theme_setting_system_primary_color_title": "Sistem rengini kullan", "theme_setting_system_theme_switch": "Otomatik (sistem ayarına göre)", "theme_setting_theme_subtitle": "Uygulama teması seç", - "theme_setting_three_stage_loading_subtitle": "Üç aşamalı yükleme, yükleme performansını artırabilir ancak önemli ölçüde daha yüksek ağ yüküne sebep olur.", + "theme_setting_three_stage_loading_subtitle": "Üç aşamalı yükleme, yükleme performansını artırabilir ancak ağ yükünü önemli ölçüde artırır", "theme_setting_three_stage_loading_title": "Üç aşamalı yüklemeyi etkinleştir", "they_will_be_merged_together": "Birlikte birleştirilecekler", "third_party_resources": "Üçüncü taraf kaynaklar", @@ -1902,9 +1987,11 @@ "timezone": "Zaman dilimi", "to_archive": "Arşivle", "to_change_password": "Şifreyi değiştir", - "to_favorite": "Gözdelere ekle", + "to_favorite": "Favorilere ekle", "to_login": "Oturum aç", + "to_multi_select": "çoklu seçim için", "to_parent": "Üst öğeye git", + "to_select": "seçmek için", "to_trash": "Çöpe taşı", "toggle_settings": "Ayarları değiştir", "total": "Toplam", @@ -1913,17 +2000,18 @@ "trash_action_prompt": "{count} çöp kutusuna taşındı", "trash_all": "Hepsini sil", "trash_count": "Çöp kutusu {count, number}", - "trash_delete_asset": "Ögeyi Sil/Çöpe gönder", + "trash_delete_asset": "Öğeyi Sil/Çöpe Gönder", "trash_emptied": "Çöp kutusu temizlendi", "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 {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_no_assets": "Çöp kutusuna atılmış öğe yok", + "trash_page_restore_all": "Tümünü Geri Yükle", + "trash_page_select_assets_btn": "Öğeleri seç", "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.", + "troubleshoot": "Sorun giderme", "type": "Tür", "unable_to_change_pin_code": "PIN kodu değiştirilemedi", "unable_to_setup_pin_code": "PIN kodu ayarlanamadı", @@ -1931,45 +2019,46 @@ "unarchive_action_prompt": "{count} Arşivden kaldırıldı", "unarchived_count": "{count, plural, other {# arşivden çıkarıldı}}", "undo": "Geri al", - "unfavorite": "Gözdelerden kaldır", - "unfavorite_action_prompt": "{count} Sık Kullanılanlar'dan kaldırıldı", + "unfavorite": "Favorilerden kaldır", + "unfavorite_action_prompt": "{count} Favorilerden kaldırıldı", "unhide_person": "Kişiyi göster", "unknown": "Bilinmeyen", - "unknown_country": "Bilinmeyen ülke", - "unknown_year": "Bilinmeyen YIl", + "unknown_country": "Bilinmeyen Ülke", + "unknown_year": "Bilinmeyen Yıl", "unlimited": "Sınırsız", "unlink_motion_video": "Hareketli video bağlantısını kaldır", "unlink_oauth": "OAuth bağlantısını kaldır", "unlinked_oauth_account": "Bağlantısı kaldırılmış OAuth hesabı", - "unmute_memories": "Anıların sesini aç", + "unmute_memories": "Anıların Sesini Aç", "unnamed_album": "İsimsiz Albüm", "unnamed_album_delete_confirmation": "Bu albümü silmek istediğinizden emin misiniz?", - "unnamed_share": "İsimsiz paylaşım", + "unnamed_share": "İsimsiz Paylaşım", "unsaved_change": "Kaydedilmemiş değişiklik", "unselect_all": "Tümünü seçimini kaldır", "unselect_all_duplicates": "Tüm çiftlerin seçimini kaldır", "unselect_all_in": "{group} içindeki tüm seçimleri kaldır", "unstack": "Yığını kaldır", "unstack_action_prompt": "{count} istiflenmemiş", - "unstacked_assets_count": "{count, plural, one {# dosya} other {# dosya}} yığını kaldırıldı", + "unstacked_assets_count": "{count, plural, one {# öğenin} other {# öğelerin}} yığını kaldırıldı", "untagged": "Etiketlenmemiş", "up_next": "Sıradaki", + "update_location_action_prompt": "Seçilen {count} öğenin konumunu şu şekilde güncelleyin:", "updated_at": "Güncellenme", - "updated_password": "Şifreyi güncelle", + "updated_password": "Güncellenen şifre", "upload": "Yükle", "upload_action_prompt": "{count} yükleme için sıraya alındı", "upload_concurrency": "Yükleme eşzamanlılığı", "upload_details": "Yükleme Ayrıntıları", "upload_dialog_info": "Seçili öğeleri sunucuya yedeklemek istiyor musunuz?", "upload_dialog_title": "Öğe Yükle", - "upload_errors": "{count, plural, one {# hata} other {# hatayla}} yükleme tamamlandı, yeni yüklenen dosyaları görmek için sayfayı güncelleyin.", + "upload_errors": "{count, plural, one {# hata} other {# hatayla}} yükleme tamamlandı, yeni yüklenen öğeleri görmek için sayfayı güncelleyin.", "upload_finished": "Yükleme tamamlandı", "upload_progress": "{remaining, number} kalan - {processed, number}/{total, number} işlendi", - "upload_skipped_duplicates": "{count, plural, one {# çift dosya} other {# çift dosya}} atlandı", + "upload_skipped_duplicates": "{count, plural, one {# yinelenen öğe} other {# yinelenen öğeler}} atlandı", "upload_status_duplicates": "Çiftler", "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_success": "Yükleme başarılı, yüklenen yeni öğeleri görebilmek için sayfayı yenileyin.", "upload_to_immich": "Immich'e Yükle ({count})", "uploading": "Yükleniyor", "uploading_media": "Medya yükleme", @@ -1981,7 +2070,7 @@ "user": "Kullanıcı", "user_has_been_deleted": "Bu kullanıcı silindi.", "user_id": "Kullanıcı ID", - "user_liked": "{type, select, photo {Bu fotoğraf} video {Bu video} asset {Bu dosya} other {Bu}} {user} tarafından beğenildi", + "user_liked": "{type, select, photo {Bu fotoğraf} video {Bu video} asset {Bu öğe} other {Bu}} {user} tarafından beğenildi", "user_pin_code_settings": "PIN Kodu", "user_pin_code_settings_description": "PIN kodunuzu yönetin", "user_privacy": "Kullanıcı Gizliliği", @@ -1990,21 +2079,21 @@ "user_role_set": "{user}, {role} olarak ayarlandı", "user_usage_detail": "Kullanıcı kullanım detayı", "user_usage_stats": "Hesap kullanım istatistikleri", - "user_usage_stats_description": "hesap kullanım istatistiklerini göster", + "user_usage_stats_description": "Hesap kullanım istatistiklerini göster", "username": "Kullanıcı adı", "users": "Kullanıcılar", "users_added_to_album_count": "Albüme {count, plural, one {# user} other {# users}} eklendi", - "utilities": "Yardımcılar", + "utilities": "Yardımcı Programlar", "validate": "Doğrula", "validate_endpoint_error": "Lütfen geçerli bir URL girin", "variables": "Değişkenler", - "version": "Versiyon", + "version": "Sürüm", "version_announcement_closing": "Arkadaşınız, Alex", "version_announcement_message": "Merhaba! Immich'in yeni bir sürümü mevcut. Lütfen yapılandırmanızın güncel olduğundan emin olmak için sürüm notlarını okumak için biraz zaman ayırın, özellikle WatchTower veya Immich kurulumunuzu otomatik olarak güncelleyen bir mekanizma kullanıyorsanız yanlış yapılandırmaların önüne geçmek adına bu önemlidir.", - "version_history": "Versiyon geçmişi", + "version_history": "Sürüm Geçmişi", "version_history_item": "{version}, {date} tarihinde kuruldu", "video": "Video", - "video_hover_setting": "Üzerinde durulduğunda video önizlemesi oynat", + "video_hover_setting": "Üzerinde durulduğunda video ön izlemesi 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", "videos_count": "{count, plural, one {# video} other {# video}}", @@ -2017,9 +2106,10 @@ "view_link": "Bağlantıyı göster", "view_links": "Bağlantıları göster", "view_name": "Göster", - "view_next_asset": "Sonraki dosyayı görüntüle", - "view_previous_asset": "Önceki dosyayı görüntüle", + "view_next_asset": "Sonraki öğeyi görüntüle", + "view_previous_asset": "Önceki öğeyi görüntüle", "view_qr_code": "QR kodu görüntüle", + "view_similar_photos": "Benzer fotoğrafları görüntüle", "view_stack": "Yığını görüntüle", "view_user": "Kullanıcıyı Görüntüle", "viewer_remove_from_stack": "Yığından Kaldır", @@ -2038,5 +2128,6 @@ "yes": "Evet", "you_dont_have_any_shared_links": "Herhangi bir paylaşılan bağlantınız yok", "your_wifi_name": "Wi-Fi Adınız", - "zoom_image": "Görüntüyü yakınlaştır" + "zoom_image": "Görüntüyü yakınlaştır", + "zoom_to_bounds": "Sınırlara yakınlaştır" } diff --git a/i18n/uk.json b/i18n/uk.json index 903c83ec25..a664fc9aa0 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -28,6 +28,7 @@ "add_to_album": "Додати у альбом", "add_to_album_bottom_sheet_added": "Додано до {album}", "add_to_album_bottom_sheet_already_exists": "Вже є в {album}", + "add_to_album_bottom_sheet_some_local_assets": "Деякі локальні ресурси не вдалося додати до альбому", "add_to_album_toggle": "Перемикання вибору для {album}", "add_to_albums": "Додати до альбомів", "add_to_albums_count": "Додати до альбомів ({count})", @@ -39,7 +40,7 @@ "admin": { "add_exclusion_pattern_description": "Додайте шаблони виключень. Підстановка з використанням *, ** та ? підтримується. Для ігнорування всіх файлів у будь-якому каталозі з ім'ям «Raw», використовуйте \"**/Raw/**\". Для ігнорування всіх файлів, що закінчуються на \".tif\", використовуйте \"**/*.tif\". Для ігнорування абсолютного шляху використовуйте \"/path/to/ignore/**\".", "admin_user": "Адміністратор", - "asset_offline_description": "Цей файл зовнішньої бібліотеки не знайдено на диску і був переміщений до смітника. Якщо файл був переміщений у межах бібліотеки, перевірте свою стрічку на наявність нового відповідного файлу. Щоб відновити цей файл, переконайтеся, що шлях до файлу доступний для Immich, і проскануйте бібліотеку.", + "asset_offline_description": "Цей файл зовнішньої бібліотеки не знайдено на диску і був переміщений до кошика. Якщо файл був переміщений у межах бібліотеки, перевірте свою стрічку на наявність нового відповідного файлу. Щоб відновити цей файл, переконайтеся, що шлях до файлу доступний для Immich, і проскануйте бібліотеку.", "authentication_settings": "Налаштування аутентифікації", "authentication_settings_description": "Управління паролями, OAuth та іншими налаштуваннями аутентифікації", "authentication_settings_disable_all": "Ви впевнені, що хочете вимкнути всі методи входу? Вхід буде повністю вимкнений.", @@ -70,14 +71,14 @@ "cron_expression_description": "Встановіть інтервал сканування, використовуючи формат cron. Для отримання додаткової інформації зверніться до напр. Crontab Guru", "cron_expression_presets": "Попередні налаштування cron виразів", "disable_login": "Вимкнути вхід", - "duplicate_detection_job_description": "Запустити машинне навчання на активах для виявлення схожих зображень. Залежить від інтелектуального пошуку", + "duplicate_detection_job_description": "Запустити машинне навчання на ресурсах для виявлення схожих зображень. Використовує інтелектуальний пошук", "exclusion_pattern_description": "Шаблони виключень дозволяють ігнорувати файли та папки під час сканування вашої бібліотеки. Це корисно, якщо у вас є папки, які містять файли, які ви не хочете імпортувати, наприклад, RAW-файли.", "external_library_management": "Керування зовнішніми бібліотеками", "face_detection": "Виявлення обличчя", "face_detection_description": "Виявлення облич на медіафайлах за допомогою машинного навчання. Для відео обробляється лише ескіз. \"Оновити\" повторно обробляє всі файли. \"Скинути\" додатково очищає всі поточні дані про обличчя. \"Відсутні\" ставить у чергу файли, які ще не були оброблені. Виявлені обличчя будуть поставлені в чергу для розпізнавання після завершення виявлення, групуючи їх у вже існуючих або нових людей.", "facial_recognition_job_description": "Групування виявлених облич у людей. Цей крок виконується після завершення виявлення облич. \"Скинути\" повторно кластеризує всі обличчя. \"Відсутні\" ставить у чергу обличчя, яким ще не призначено людину.", "failed_job_command": "Команда {command} не виконалася для завдання: {job}", - "force_delete_user_warning": "ПОПЕРЕДЖЕННЯ: Це негайно призведе до видалення користувача і всіх активів. Цю дію не можна скасувати, і файли не можна буде відновити.", + "force_delete_user_warning": "ПОПЕРЕДЖЕННЯ: Це негайно призведе до видалення користувача і всіх ресурсів. Цю дію не можна скасувати, і файли не можна буде відновити.", "image_format": "Формат", "image_format_description": "Формат WebP виробляє меньші файлів, ніж JPEG, але його кодування вимагає більше часу.", "image_fullsize_description": "Повнорозмірне зображення з видаленими метаданими, які використовуються під час збільшення", @@ -123,6 +124,13 @@ "logging_enable_description": "Увімкнути ведення журналу", "logging_level_description": "Коли увімкнено, який рівень журналювання використовувати.", "logging_settings": "Журналювання", + "machine_learning_availability_checks": "Перевірки доступності", + "machine_learning_availability_checks_description": "Автоматично виявляти та надавати перевагу доступним серверам машинного навчання", + "machine_learning_availability_checks_enabled": "Увімкнути перевірки доступності", + "machine_learning_availability_checks_interval": "Інтервал перевірки", + "machine_learning_availability_checks_interval_description": "Інтервал у мілісекундах між перевірками доступності", + "machine_learning_availability_checks_timeout": "Тайм-аут запиту", + "machine_learning_availability_checks_timeout_description": "Тайм-аут у мілісекундах для перевірки доступності", "machine_learning_clip_model": "Модель CLIP", "machine_learning_clip_model_description": "Ім'я однієї з моделей CLIP, яка перерахована тут. Зауважте, що потрібно знову запустити завдання «Розумний пошук» для всіх зображень після зміни моделі.", "machine_learning_duplicate_detection": "Виявлення дублікатів", @@ -258,10 +266,10 @@ "server_welcome_message": "Вітальне повідомлення", "server_welcome_message_description": "Повідомлення, яке відображається на сторінці входу.", "sidecar_job": "Метадані з sidecar-файлів", - "sidecar_job_description": "Виявлення або синхронізація метаданих додатків з файлової системи", + "sidecar_job_description": "Пошук або синхронізація сайдкар-метаданих з файлової системи", "slideshow_duration_description": "Кількість секунд для відображення кожного зображення", "smart_search_job_description": "Запуск машинного навчання для ресурсів для підтримки розумного пошуку", - "storage_template_date_time_description": "Позначка часу створення активу використовується для інформації про дату й час", + "storage_template_date_time_description": "Позначка часу створення ресурсу використовується для інформації про дату й час", "storage_template_date_time_sample": "Час вибірки {date}", "storage_template_enable_description": "Ввімкнути механізм шаблонів сховища", "storage_template_hash_verification_enabled": "Увімкнено перевірку хешу", @@ -340,7 +348,7 @@ "transcoding_settings": "Налаштування транскодування відео", "transcoding_settings_description": "Керування які відео транскодувати і як їх обробляти", "transcoding_target_resolution": "Роздільна здатність", - "transcoding_target_resolution_description": "Вищі роздільні здатності можуть зберігати більше деталей, але займають більше часу на кодування, мають більші розміри файлів і можуть зменшити швидкість роботи додатку.", + "transcoding_target_resolution_description": "Вищі роздільні здатності можуть зберігати більше деталей, але займають більше часу на кодування, мають більші розміри файлів і можуть зменшити швидкість роботи застосунку.", "transcoding_temporal_aq": "Тимчасове AQ", "transcoding_temporal_aq_description": "Це застосовується лише до NVENC. Підвищує якість сцен з великою деталізацією та низьким рухом. Може бути несумісним зі старими пристроями.", "transcoding_threads": "Потоки", @@ -353,11 +361,11 @@ "transcoding_two_pass_encoding_setting_description": "Транскодування за двома проходами для отримання кращих закодованих відео. Коли ввімкнено максимальний бітрейт (необхідний для роботи з H.264 та HEVC), цей режим використовує діапазон бітрейту, заснований на максимальному бітрейті, і ігнорує CRF. Для VP9 можна використовувати CRF, якщо вимкнено максимальний бітрейт.", "transcoding_video_codec": "Відеокодек", "transcoding_video_codec_description": "VP9 має високу ефективність і сумісність з вебом, але потребує більше часу на транскодування. HEVC працює схоже, але має меншу сумісність з вебом. H.264 має широку сумісність і швидко транскодується, але створює значно більші файли. AV1 - найефективніший кодек, але не підтримується на старіших пристроях.", - "trash_enabled_description": "Увімкнення смітника", + "trash_enabled_description": "Увімкнення кошика", "trash_number_of_days": "Кількість днів", - "trash_number_of_days_description": "Кількість днів, щоб залишити ресурси в смітнику перед остаточним їх видаленням", - "trash_settings": "Налаштування смітника", - "trash_settings_description": "Керування налаштуваннями смітника", + "trash_number_of_days_description": "Кількість днів, протягом якої залишати ресурси в кошику перед їх остаточним видаленням", + "trash_settings": "Налаштування кошика", + "trash_settings_description": "Керування налаштуваннями кошика", "unlink_all_oauth_accounts": "Від’єднати всі облікові записи OAuth", "unlink_all_oauth_accounts_description": "Не забудьте від’єднати всі облікові записи OAuth перед переходом до нового постачальника.", "unlink_all_oauth_accounts_prompt": "Ви впевнені, що хочете від’єднати всі облікові записи OAuth? Це скине ідентифікатор OAuth для кожного користувача, і цю дію не можна буде скасувати.", @@ -387,15 +395,15 @@ "admin_password": "Пароль адміністратора", "administration": "Адміністрування", "advanced": "Розширені", - "advanced_settings_beta_timeline_subtitle": "Випробуйте новий інтерфейс застосунку", - "advanced_settings_beta_timeline_title": "Бета-версія стрічки", - "advanced_settings_enable_alternate_media_filter_subtitle": "Використовуйте цей варіант для фільтрації медіафайлів під час синхронізації за альтернативними критеріями. Спробуйте це, якщо у вас виникають проблеми з тим, що додаток не виявляє всі альбоми.", + "advanced_settings_enable_alternate_media_filter_subtitle": "Використовуйте цей варіант для фільтрації медіафайлів під час синхронізації за альтернативними критеріями. Спробуйте це, якщо у вас виникають проблеми з тим, що застосунок не виявляє всі альбоми.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛЬНИЙ] Використовуйте альтернативний фільтр синхронізації альбомів пристрою", "advanced_settings_log_level_title": "Рівень логування: {level}", "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте цей параметр, щоб завантажувати зображення з серверу.", "advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням", "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом", "advanced_settings_proxy_headers_title": "Проксі-заголовки", + "advanced_settings_readonly_mode_subtitle": "Увімкнення режиму тільки для читання, в якому фотографії можна тільки переглядати, а такі функції, як вибір декількох зображень, спільний доступ, передача, видалення, вимкнені. Увімкнення/вимкнення режиму тільки для читання за допомогою аватара користувача на головному екрані", + "advanced_settings_readonly_mode_title": "Режим лише для читання", "advanced_settings_self_signed_ssl_subtitle": "Пропускає перевірку SSL-сертифіката сервера. Потрібне для самопідписаних сертифікатів.", "advanced_settings_self_signed_ssl_title": "Дозволити самопідписані SSL-сертифікати", "advanced_settings_sync_remote_deletions_subtitle": "Автоматично видаляти або відновлювати ресурс на цьому пристрої, коли ця дія виконується в веб-інтерфейсі", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "Ви впевнені, що хочете видалити {user}?", "album_search_not_found": "Альбомів, що відповідають вашому запиту, не знайдено", "album_share_no_users": "Схоже, ви поділилися цим альбомом з усіма користувачами або у вас немає жодного користувача, з яким можна було б поділитися.", + "album_summary": "Короткий опис альбому", "album_updated": "Альбом оновлено", "album_updated_setting_description": "Отримуйте сповіщення на електронну пошту, коли у спільному альбомі з'являються нові ресурси", "album_user_left": "Ви покинули {album}", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "Вийти з аккаунта", "app_settings": "Налаштування програми", "appears_in": "З'являється в", + "apply_count": "Застосувати ({count, number})", "archive": "Архівувати", "archive_action_prompt": "{count} додано до архіву", "archive_or_unarchive_photo": "Архівувати або розархівувати фото", @@ -488,11 +498,13 @@ "asset_list_layout_sub_title": "Розмітка", "asset_list_settings_subtitle": "Налаштування вигляду сітки фото", "asset_list_settings_title": "Фото-сітка", - "asset_offline": "Актив вимкнено", - "asset_offline_description": "Цей зовнішній актив більше не знайдено на диску. Будь ласка, зверніться до адміністратора Immich за допомогою.", + "asset_offline": "Ресурс офлайн", + "asset_offline_description": "Цей зовнішній ресурс більше не знайдено на диску. Будь ласка, зверніться до адміністратора Immich за допомогою.", "asset_restored_successfully": "Елемент успішно відновлено", "asset_skipped": "Пропущено", - "asset_skipped_in_trash": "У смітнику", + "asset_skipped_in_trash": "У кошику", + "asset_trashed": "Об'єкт видалено з кошика", + "asset_troubleshoot": "Вирішення проблем з активами", "asset_uploaded": "Завантажено", "asset_uploading": "Завантаження…", "asset_viewer_settings_subtitle": "Керуйте налаштуваннями переглядача галереї", @@ -500,34 +512,36 @@ "assets": "елементи", "assets_added_count": "Додано {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_added_to_album_count": "Додано {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} до альбому", - "assets_added_to_albums_count": "Додано {assetTotal, plural, one {# актив} other {# активи}} до {albumTotal} альбомів", + "assets_added_to_albums_count": "Додано {assetTotal, plural, one {# ресурс} other {# ресурси}} до {albumTotal, plural, one {# альбом} other {# альбом}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Ресурс} other {Ресурси}} не можна додати до альбому", - "assets_cannot_be_added_to_albums": "{count, plural, one {Актив} other {Активи}} не можна додати до жодного з альбомів", + "assets_cannot_be_added_to_albums": "{count, plural, one {Елемент} other {Елементи}} не можна додати до жодного з альбомів", "assets_count": "{count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_deleted_permanently": "{count} елемент(и) остаточно видалено", "assets_deleted_permanently_from_server": "{count} елемент(и) видалено назавжди з сервера Immich", "assets_downloaded_failed": "{count, plural, one {Завантажено # файл — {error} файл не вдалося} other {Завантажено # файлів — {error} файлів не вдалося}}", "assets_downloaded_successfully": "{count, plural, one {Успішно завантажено # файл} other {Успішно завантажено # файлів}}", - "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у смітник", + "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у кошик", "assets_permanently_deleted_count": "Остаточно видалено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_removed_count": "Вилучено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_removed_permanently_from_device": "{count} елемент(и) видалені назавжди з вашого пристрою", - "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої активи з смітника? Цю дію не можна скасувати! Зверніть увагу, що будь-які офлайн-активи не можуть бути відновлені таким чином.", + "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої елементи з кошика? Цю дію не можна скасувати! Зверніть увагу, що жодні офлайн ресурси не можуть бути відновлені таким чином.", "assets_restored_count": "Відновлено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_restored_successfully": "{count} елемент(и) успішно відновлено", "assets_trashed": "{count} елемент(и) поміщено до кошика", - "assets_trashed_count": "Поміщено в смітник {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", + "assets_trashed_count": "Поміщено в кошик {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_trashed_from_server": "{count} елемент(и) поміщено до кошика на сервері Immich", "assets_were_part_of_album_count": "{count, plural, one {Ресурс був} few {Ресурси були} other {Ресурси були}} вже частиною альбому", - "assets_were_part_of_albums_count": "{count, plural, one {Актив був} other {Активи були}} вже є частиною альбомів", + "assets_were_part_of_albums_count": "{count, plural, one {Елемент вже був} other {Елементи вже були}} частиною альбомів", "authorized_devices": "Авторизовані пристрої", "automatic_endpoint_switching_subtitle": "Підключатися локально через зазначену Wi-Fi мережу, коли це можливо, і використовувати альтернативні з'єднання в інших випадках", "automatic_endpoint_switching_title": "Автоматичне перемикання URL", "autoplay_slideshow": "Автоматичне відтворення слайдшоу", "back": "Назад", "back_close_deselect": "Повернутися, закрити або скасувати вибір", + "background_backup_running_error": "Наразі виконується фонове резервне копіювання, неможливо розпочати резервне копіювання вручну", "background_location_permission": "Дозвіл до місцезнаходження у фоні", "background_location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має *завжди* мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", + "background_options": "Параметри фону", "backup": "Резервне копіювання", "backup_album_selection_page_albums_device": "Альбоми на пристрої ({count})", "backup_album_selection_page_albums_tap": "Торкніться, щоб включити, двічі, щоб виключити", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "Оберіть альбоми", "backup_album_selection_page_selection_info": "Інформація про обране", "backup_album_selection_page_total_assets": "Загальна кількість унікальних елементів", + "backup_albums_sync": "Синхронізація резервних копій альбомів", "backup_all": "Усі", "backup_background_service_backup_failed_message": "Не вдалося зробити резервну копію елементів. Повторюю…", "backup_background_service_connection_failed_message": "Не вдалося зв'язатися із сервером. Повторюю…", @@ -584,6 +599,7 @@ "backup_controller_page_turn_on": "Увімкнути резервне копіювання в активному режимі", "backup_controller_page_uploading_file_info": "Завантажую інформацію про файл", "backup_err_only_album": "Не можу видалити єдиний альбом", + "backup_error_sync_failed": "Помилка синхронізації. Не вдається обробити резервну копію.", "backup_info_card_assets": "елементи", "backup_manual_cancelled": "Скасовано", "backup_manual_in_progress": "Завантаження вже відбувається. Спробуйте згодом", @@ -594,8 +610,6 @@ "backup_setting_subtitle": "Управління налаштуваннями завантаження у фоновому та активному режимі", "backup_settings_subtitle": "Керування налаштуваннями завантаження", "backward": "Зворотній", - "beta_sync": "Стан бета-синхронізації", - "beta_sync_subtitle": "Налаштування нової системи синхронізації", "biometric_auth_enabled": "Біометрична автентифікація увімкнена", "biometric_locked_out": "Вам закрито доступ до біометричної автентифікації", "biometric_no_options": "Біометричні параметри недоступні", @@ -608,12 +622,12 @@ "build_image": "Версія збірки", "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", - "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в смітник {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в смітник всі інші дублікати.", + "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в кошик {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в кошик всі інші дублікати.", "buy": "Придбайте Immich", "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_subtitle": "Фото та відео, які ігноруються застосунком", "cache_settings_duplicated_assets_title": "Дубльовані елементи ({count})", "cache_settings_statistics_album": "Бібліотечні мініатюри", "cache_settings_statistics_full": "Повнорзомірні зображення", @@ -653,9 +667,11 @@ "change_pin_code": "Змінити PIN-код", "change_your_password": "Змініть свій пароль", "changed_visibility_successfully": "Видимість успішно змінено", - "check_corrupt_asset_backup": "Перевірити на пошкоджені резервні копії активів", + "charging": "Зарядка", + "charging_requirement_mobile_backup": "Для фонового резервного копіювання пристрій повинен заряджатися", + "check_corrupt_asset_backup": "Перевірити на пошкоджені резервні копії ресурсів", "check_corrupt_asset_backup_button": "Виконати перевірку", - "check_corrupt_asset_backup_description": "Запустіть цю перевірку лише через Wi-Fi та після того, як всі активи будуть завантажені на сервер. Процес може зайняти кілька хвилин.", + "check_corrupt_asset_backup_description": "Запустити цю перевірку лише через Wi-Fi та після того, як всі ресурси будуть завантажені на сервер. Процес може зайняти кілька хвилин.", "check_logs": "Перевірити журнали", "choose_matching_people_to_merge": "Виберіть людей для об'єднання", "city": "Місто", @@ -688,7 +704,7 @@ "completed": "Завершено", "confirm": "Підтвердіть", "confirm_admin_password": "Підтвердити пароль адміністратора", - "confirm_delete_face": "Ви впевнені, що хочете видалити обличчя {name} з активу?", + "confirm_delete_face": "Ви впевнені, що хочете видалити обличчя {name} з елементу?", "confirm_delete_shared_link": "Ви впевнені, що хочете видалити це спільне посилання?", "confirm_keep_this_delete_others": "Усі інші ресурси в стеку буде видалено, окрім цього ресурсу. Ви впевнені, що хочете продовжити?", "confirm_new_pin_code": "Підтвердьте новий PIN-код", @@ -729,7 +745,7 @@ "create_link_to_share_description": "Дозволити перегляд вибраних фотографій за посиланням будь-кому", "create_new": "СТВОРИТИ НОВИЙ", "create_new_person": "Створити нову особу", - "create_new_person_hint": "Призначити обраним активам нову особу", + "create_new_person_hint": "Призначити обраним елементам нову особу", "create_new_user": "Створити нового користувача", "create_shared_album_page_share_add_assets": "ДОДАТИ ЕЛЕМЕНТИ", "create_shared_album_page_share_select_photos": "Вибрати фото", @@ -739,6 +755,7 @@ "create_user": "Створити користувача", "created": "Створено", "created_at": "Створено", + "creating_linked_albums": "Створення пов’язаних альбомів...", "crop": "Кадрувати", "curated_object_page_title": "Речі", "current_device": "Поточний пристрій", @@ -794,7 +811,7 @@ "delete_tag_confirmation_prompt": "Ви впевнені, що хочете видалити тег {tagName}?", "delete_user": "Видалити користувача", "deleted_shared_link": "Видалено загальне посилання", - "deletes_missing_assets": "Видаляє активи, які відсутні на диску", + "deletes_missing_assets": "Видаляє ресурси, які відсутні на диску", "description": "Опис", "description_input_hint_text": "Додати опис...", "description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць", @@ -874,8 +891,8 @@ "email": "Електронна пошта", "email_notifications": "Сповіщення ел. поштою", "empty_folder": "Ця папка порожня", - "empty_trash": "Очистити смітник", - "empty_trash_confirmation": "Ви впевнені, що хочете очистити смітник? Це остаточно видалить всі ресурси в смітнику з Immich.\nЦю дію не можна скасувати!", + "empty_trash": "Очистити кошик", + "empty_trash_confirmation": "Ви впевнені, що хочете очистити кошик? Це остаточно видалить всі ресурси в кошику з Immich.\nЦю дію не можна скасувати!", "enable": "Увімкнути", "enable_backup": "Увімкнути резервне копіювання", "enable_biometric_auth_description": "Введіть свій PIN-код, щоб увімкнути біометричну автентифікацію", @@ -887,8 +904,10 @@ "enter_your_pin_code_subtitle": "Введіть свій PIN-код, щоб отримати доступ до особистої папки", "error": "Помилка", "error_change_sort_album": "Не вдалося змінити порядок сортування альбому", - "error_delete_face": "Помилка при видаленні обличчя з активу", + "error_delete_face": "Помилка при видаленні обличчя з елементу", + "error_getting_places": "Помилка отримання місць", "error_loading_image": "Помилка завантаження зображення", + "error_loading_partners": "Помилка завантаження партнерів: {error}", "error_saving_image": "Помилка: {error}", "error_tag_face_bounding_box": "Помилка під час позначення обличчя – не вдалося отримати координати рамки", "error_title": "Помилка: щось пішло не так", @@ -964,7 +983,7 @@ "unable_to_download_files": "Неможливо завантажити файли", "unable_to_edit_exclusion_pattern": "Не вдалося редагувати шаблон виключення", "unable_to_edit_import_path": "Неможливо відредагувати шлях імпорту", - "unable_to_empty_trash": "Неможливо очистити смітник", + "unable_to_empty_trash": "Неможливо очистити кошик", "unable_to_enter_fullscreen": "Неможливо увійти в повноекранний режим", "unable_to_exit_fullscreen": "Неможливо вийти з повноекранного режиму", "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів", @@ -988,7 +1007,7 @@ "unable_to_reset_password": "Не вдається скинути пароль", "unable_to_reset_pin_code": "Неможливо скинути PIN-код", "unable_to_resolve_duplicate": "Не вдається вирішити дублікат", - "unable_to_restore_assets": "Неможливо відновити активи", + "unable_to_restore_assets": "Неможливо відновити елементи", "unable_to_restore_trash": "Не вдалося відновити вміст", "unable_to_restore_user": "Не вдається відновити користувача", "unable_to_save_album": "Не вдається зберегти альбом", @@ -1002,7 +1021,7 @@ "unable_to_set_feature_photo": "Не вдалося встановити фотографію на обкладинку", "unable_to_set_profile_picture": "Не вдається встановити зображення профілю", "unable_to_submit_job": "Не вдалося відправити завдання", - "unable_to_trash_asset": "Неможливо вилучити актив", + "unable_to_trash_asset": "Неможливо видалити елемент", "unable_to_unlink_account": "Не вдається відв'язати обліковий запис", "unable_to_unlink_motion_video": "Не вдається від'єднати рухоме відео", "unable_to_update_album_cover": "Неможливо оновити обкладинку альбому", @@ -1040,7 +1059,7 @@ "external": "Зовнішні", "external_libraries": "Зовнішні бібліотеки", "external_network": "Зовнішня мережа", - "external_network_sheet_info": "Коли ви не підключені до переважної мережі Wi-Fi, додаток підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", + "external_network_sheet_info": "Коли ви не підключені до обраної мережі Wi-Fi, застосунок підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", "face_unassigned": "Не призначено", "failed": "Не вдалося", "failed_to_authenticate": "Помилка автентифікації", @@ -1053,7 +1072,8 @@ "favorites_page_no_favorites": "Немає улюблених елементів", "feature_photo_updated": "Вибране фото оновлено", "features": "Додаткові можливості", - "features_setting_description": "Керування додатковими можливостями додатка", + "features_in_development": "Функції в розробці", + "features_setting_description": "Керування додатковими можливостями застосунку", "file_name": "Ім'я файлу", "file_name_or_extension": "Ім'я файлу або розширення", "filename": "Ім'я файлу", @@ -1073,12 +1093,15 @@ "gcast_enabled": "Google Cast'", "gcast_enabled_description": "Ця функція завантажує зовнішні ресурси з Google для своєї роботи.", "general": "Загальні", + "geolocation_instruction_location": "Натисніть на об'єкт із GPS-координатами, щоб використати його місцезнаходження, або виберіть місцезнаходження безпосередньо на карті", "get_help": "Отримати допомогу", "get_wifiname_error": "Не вдалося отримати назву Wi-Fi. Переконайтеся, що ви надали необхідні дозволи та підключені до Wi-Fi мережі", "getting_started": "Початок", "go_back": "Повернутися назад", "go_to_folder": "Перейти до папки", "go_to_search": "Перейти до пошуку", + "gps": "GPS", + "gps_missing": "Немає GPS", "grant_permission": "Надати дозвіл", "group_albums_by": "Групувати альбоми за...", "group_country": "Групувати за країною", @@ -1116,7 +1139,7 @@ "home_page_delete_remote_err_local": "Локальні елемент(и) вже в процесі видалення з сервера, пропущено", "home_page_favorite_err_local": "Поки що не можна додати до улюблених локальні елементи, пропущено", "home_page_favorite_err_partner": "Поки що не можна додати до улюблених елементи партнера, пропущено", - "home_page_first_time_notice": "Якщо ви користуєтеся додатком вперше, будь ласка, оберіть альбом для резервного копіювання, щоб на шкалі часу з’явилися фото та відео", + "home_page_first_time_notice": "Якщо ви користуєтеся застосунком вперше, будь ласка, оберіть альбом для резервного копіювання, щоб на шкалі часу з’явилися фото та відео", "home_page_locked_error_local": "Не вдається перемістити локальні файли до особистої папки, пропускається", "home_page_locked_error_partner": "Не вдається перемістити партнерські файли до особистої папки, пропускається", "home_page_share_err_local": "Неможливо поділитися локальними елементами через посилання, пропущено", @@ -1151,7 +1174,7 @@ "in_archive": "В архіві", "include_archived": "Відображати архів", "include_shared_albums": "Включити спільні альбоми", - "include_shared_partner_assets": "Включайте спільні партнерські активи", + "include_shared_partner_assets": "Включайте спільні партнерські ресурси", "individual_share": "Індивідуальний доступ", "individual_shares": "Окремі спільні доступи", "info": "Інформація", @@ -1214,8 +1237,9 @@ "local": "На пристрої", "local_asset_cast_failed": "Неможливо транслювати ресурс, який не завантажено на сервер", "local_assets": "Локальні фото та відео", + "local_media_summary": "Зведення місцевих ЗМІ", "local_network": "Локальна мережа", - "local_network_sheet_info": "Додаток підключатиметься до сервера через цей URL, коли використовується вказана Wi-Fi мережа", + "local_network_sheet_info": "Застосунок підключатиметься до сервера через цей URL, коли використовується вказана Wi-Fi мережа", "location_permission": "Дозвіл до місцезнаходження", "location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має завжди мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", "location_picker_choose_on_map": "Обрати на мапі", @@ -1225,6 +1249,7 @@ "location_picker_longitude_hint": "Вкажіть довготу", "lock": "Заблокувати", "locked_folder": "Особиста папка", + "log_detail_title": "Деталі журналу", "log_out": "Вийти", "log_out_all_devices": "Вийти з усіх пристроїв", "logged_in_as": "Вхід виконано як {user}", @@ -1255,6 +1280,7 @@ "login_password_changed_success": "Пароль оновлено успішно", "logout_all_device_confirmation": "Ви впевнені, що хочете вийти з усіх пристроїв?", "logout_this_device_confirmation": "Ви впевнені, що хочете вийти з цього пристрою?", + "logs": "Журнали", "longitude": "Довгота", "look": "Дивитися", "loop_videos": "Циклічні відео", @@ -1262,6 +1288,7 @@ "main_branch_warning": "Ви використовуєте версію для розробників; настійно рекомендуємо використовувати релізну версію!", "main_menu": "Головне меню", "make": "Виробник", + "manage_geolocation": "Керувати місцезнаходженням", "manage_shared_links": "Керування спільними посиланнями", "manage_sharing_with_partners": "Керуйте спільним використанням з партнерами", "manage_the_app_settings": "Керування налаштуваннями програми", @@ -1296,6 +1323,7 @@ "mark_as_read": "Позначити як прочитане", "marked_all_as_read": "Позначено всі як прочитані", "matches": "Збіги", + "matching_assets": "Відповідні активи", "media_type": "Тип медіа", "memories": "Спогади", "memories_all_caught_up": "Це все на сьогодні", @@ -1325,9 +1353,9 @@ "move_to_lock_folder_action_prompt": "{count} додано до захищеної теки", "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": "Перенесено до смітника", + "moved_to_archive": "Переміщено {count, plural, one {# елемент} other {# елементів}} в архів", + "moved_to_library": "Переміщено {count, plural, one {# елемент} other {# елементів}} в бібліотеку", + "moved_to_trash": "Перенесено до кошика", "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено", "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено", "mute_memories": "Приглушити спогади", @@ -1336,6 +1364,7 @@ "name_or_nickname": "Ім'я або псевдонім", "network_requirement_photos_upload": "Використовувати стільникові дані для резервного копіювання фото", "network_requirement_videos_upload": "Використовувати стільникові дані для резервного копіювання відео", + "network_requirements": "Вимоги до мережі", "network_requirements_updated": "Вимоги до мережі змінилися, черга резервного копіювання очищена", "networking_settings": "Мережеві налаштування", "networking_subtitle": "Керування налаштуваннями кінцевої точки сервера", @@ -1346,6 +1375,7 @@ "new_person": "Нова людина", "new_pin_code": "Новий PIN-код", "new_pin_code_subtitle": "Ви вперше отримуєте доступ до особистої папки. Створіть PIN-код для безпечного доступу до цієї сторінки", + "new_timeline": "Нова хронологія", "new_user_created": "Створено нового користувача", "new_version_available": "ДОСТУПНА НОВА ВЕРСІЯ", "newest_first": "Спочатку нові", @@ -1359,20 +1389,25 @@ "no_assets_message": "НАТИСНІТЬ, ЩОБ ЗАВАНТАЖИТИ ВАШЕ ПЕРШЕ ФОТО", "no_assets_to_show": "Елементи відсутні", "no_cast_devices_found": "Пристрої для трансляції не знайдено", + "no_checksum_local": "Контрольна сума недоступна – неможливо отримати локальні ресурси", + "no_checksum_remote": "Контрольна сума недоступна – неможливо отримати віддалений ресурс", "no_duplicates_found": "Дублікатів не виявлено.", "no_exif_info_available": "Відсутня інформація про exif", "no_explore_results_message": "Завантажуйте більше фотографій, щоб насолоджуватися вашою колекцією.", "no_favorites_message": "Додавайте улюблені файли, щоб швидко знаходити ваші найкращі зображення та відео", "no_libraries_message": "Створіть зовнішню бібліотеку для перегляду фотографій і відео", + "no_local_assets_found": "З цією контрольною сумою не знайдено локальних ресурсів", "no_locked_photos_message": "Фото та відео в особистій папці приховані і не відображаються під час перегляду чи пошуку у вашій бібліотеці.", "no_name": "Без імені", "no_notifications": "Немає сповіщень", "no_people_found": "Людей, що відповідають запиту, не знайдено", "no_places": "Місць немає", + "no_remote_assets_found": "З цією контрольною сумою не знайдено віддалених ресурсів", "no_results": "Немає результатів", "no_results_description": "Спробуйте використовувати синонім або більш загальне ключове слово", "no_shared_albums_message": "Створіть альбом, щоб ділитися фотографіями та відео з людьми у вашій мережі", "no_uploads_in_progress": "Немає активних завантажень", + "not_available": "Немає даних", "not_in_any_album": "У жодному альбомі", "not_selected": "Не вибрано", "note_apply_storage_label_to_previously_uploaded assets": "Примітка: Щоб застосувати мітку сховища до раніше завантажених ресурсів, виконайте команду", @@ -1407,6 +1442,8 @@ "open_the_search_filters": "Відкрийте фільтри пошуку", "options": "Налаштування", "or": "або", + "organize_into_albums": "Упорядкувати в альбоми", + "organize_into_albums_description": "Помістити наявні фотографії в альбоми, використовуючи поточні налаштування синхронізації", "organize_your_library": "Організуйте свою бібліотеку", "original": "оригінал", "other": "Інше", @@ -1464,7 +1501,7 @@ "permission_onboarding_permission_denied": "Доступ заборонено. Для використання Immich надайте дозволи до \"Фото та відео\" в налаштуваннях.", "permission_onboarding_permission_granted": "Доступ надано! Все готово.", "permission_onboarding_permission_limited": "Доступ обмежено. Щоби дозволити Immich створювати резервні копії та керувати всією галереєю, надайте дозволи на фото й відео в налаштуваннях.", - "permission_onboarding_request": "Додатку Immich потрібен дозвіл для перегляду ваших фото та відео.", + "permission_onboarding_request": "Застосунку Immich потрібен дозвіл для перегляду ваших фото та відео.", "person": "Людина", "person_age_months": "{months, plural, one {# місяць} other {# місяці}}", "person_age_year_months": "1 year , {months, plural, one {# місяць} other {# місяці}}", @@ -1490,8 +1527,9 @@ "play_or_pause_video": "Відтворення або призупинення відео", "please_auth_to_access": "Будь ласка, пройдіть автентифікацію", "port": "Порт", - "preferences_settings_subtitle": "Керування налаштуваннями додатку", + "preferences_settings_subtitle": "Керування налаштуваннями застосунку", "preferences_settings_title": "Параметри", + "preparing": "Підготовка", "preset": "Передвстановлення", "preview": "Прев'ю", "previous": "Попереднє", @@ -1504,10 +1542,11 @@ "privacy": "Конфіденційність", "profile": "Профіль", "profile_drawer_app_logs": "Журнал", - "profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.", - "profile_drawer_client_out_of_date_minor": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мінорної версії.", + "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_readonly_mode": "Режим лише для читання ввімкнено. Щоб вийти, довго натисніть значок аватара користувача.", "profile_drawer_server_out_of_date_major": "Сервер застарів. Будь ласка, оновіть до останньої мажорної версії.", "profile_drawer_server_out_of_date_minor": "Сервер застарів. Будь ласка, оновіть до останньої мінорної версії.", "profile_image_of_user": "Зображення профілю {user}", @@ -1546,6 +1585,7 @@ "purchase_server_description_2": "Статус підтримки", "purchase_server_title": "Сервер", "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором", + "query_asset_id": "Ідентифікатор ресурсу запиту", "queue_status": "У черзі {count} з {total}", "rating": "Зоряний рейтинг", "rating_clear": "Очистити рейтинг", @@ -1553,6 +1593,9 @@ "rating_description": "Показувати рейтинг EXIF на інформаційній панелі", "reaction_options": "Опції реакції", "read_changelog": "Прочитати зміни в оновленні", + "readonly_mode_disabled": "Режим лише для читання вимкнено", + "readonly_mode_enabled": "Режим лише для читання ввімкнено", + "ready_for_upload": "Готово до завантаження", "reassign": "Перепризначити", "reassigned_assets_to_existing_person": "Перепризначено {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}} {name, select, null {існуючій особі} other {{name}}}", "reassigned_assets_to_new_person": "Перепризначено {count, plural, one {# ресурс} other {# ресурси}} новій особі", @@ -1577,6 +1620,7 @@ "regenerating_thumbnails": "Відновлення мініатюр", "remote": "На сервері", "remote_assets": "Віддалені фото та відео", + "remote_media_summary": "Зведення віддалених медіафайлів", "remove": "Вилучити", "remove_assets_album_confirmation": "Ви впевнені, що хочете видалити {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}} з альбому?", "remove_assets_shared_link_confirmation": "Ви впевнені, що хочете видалити {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}} з цього спільного посилання?", @@ -1601,7 +1645,7 @@ "removed_from_favorites_count": "{count, plural, other {Видалено #}} з обраних", "removed_memory": "Видалена пам'ять", "removed_photo_from_memory": "Фото видалене з пам'яті", - "removed_tagged_assets": "Видалено тег із {count, plural, one {# активу} other {# активів}}", + "removed_tagged_assets": "Видалено тег із {count, plural, one {# елементу} other {# елементів}}", "rename": "Перейменувати", "repair": "Ремонт", "repair_no_results_message": "Невідстежувані та відсутні файли будуть відображені тут", @@ -1625,10 +1669,11 @@ "resolved_all_duplicates": "Усі дублікати усунуто", "restore": "Відновити", "restore_all": "Відновити все", - "restore_trash_action_prompt": "{count} відновлено зі смітника", + "restore_trash_action_prompt": "{count} відновлено з кошика", "restore_user": "Відновити користувача", - "restored_asset": "Відновлений актив", + "restored_asset": "Відновлений ресурс", "resume": "Продовжити", + "resume_paused_jobs": "Відновити {count, plural, one {# призупинене завдання} other {# призупинені завдання}}", "retry_upload": "Повторити завантаження", "review_duplicates": "Переглянути дублікати", "review_large_files": "Перегляд великих файлів", @@ -1722,10 +1767,11 @@ "select_user_for_sharing_page_err_album": "Не вдалося створити альбом", "selected": "Обрано", "selected_count": "{count, plural, one {# обраний} other {# обраних}}", + "selected_gps_coordinates": "Вибрані GPS-координати", "send_message": "Надіслати повідомлення", "send_welcome_email": "Надішліть вітальний лист", "server_endpoint": "Кінцева точка сервера", - "server_info_box_app_version": "Версія додатка", + "server_info_box_app_version": "Версія застосунку", "server_info_box_server_url": "URL сервера", "server_offline": "Сервер офлайн", "server_online": "Сервер онлайн", @@ -1747,7 +1793,7 @@ "setting_image_viewer_preview_title": "Завантажувати зображення попереднього перегляду", "setting_image_viewer_title": "Зображення", "setting_languages_apply": "Застосувати", - "setting_languages_subtitle": "Змінити мову додатку", + "setting_languages_subtitle": "Змінити мову застосунку", "setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {duration}", "setting_notifications_notify_hours": "{count} годин", "setting_notifications_notify_immediately": "негайно", @@ -1850,6 +1896,7 @@ "show_slideshow_transition": "Показати перехід слайд-шоу", "show_supporter_badge": "Значок підтримки", "show_supporter_badge_description": "Показати значок підтримки", + "show_text_search_menu": "Показати меню текстового пошуку", "shuffle": "Перемішати", "sidebar": "Бічна панель", "sidebar_display_description": "Відобразити посилання на перегляд у бічній панелі", @@ -1880,6 +1927,7 @@ "stacktrace": "Стек викликів", "start": "Старт", "start_date": "Дата початку", + "start_date_before_end_date": "Дата початку має бути раніше дати завершення", "state": "Регіон", "status": "Стан", "stop_casting": "Зупинити трансляцію", @@ -1904,6 +1952,8 @@ "sync_albums_manual_subtitle": "Синхронізувати всі завантажені фото та відео у вибрані альбоми для резервного копіювання", "sync_local": "Синхронізувати на пристрої", "sync_remote": "Синхронізувати з сервером", + "sync_status": "Стан синхронізації", + "sync_status_subtitle": "Перегляд та керування системою синхронізації", "sync_upload_album_setting_subtitle": "Створюйте та завантажуйте свої фотографії та відео до вибраних альбомів на сервер Immich", "tag": "Тег", "tag_assets": "Додати теги", @@ -1912,7 +1962,7 @@ "tag_not_found_question": "Не вдається знайти тег? Створити новий тег.", "tag_people": "Тег людей", "tag_updated": "Оновлено тег: {tag}", - "tagged_assets": "Позначено тегом {count, plural, one {# актив} other {# активи}}", + "tagged_assets": "Позначено тегом {count, plural, one {# ресурс} other {# ресурси}}", "tags": "Теги", "tap_to_run_job": "Торкніться, щоб запустити завдання", "template": "Шаблон", @@ -1929,7 +1979,7 @@ "theme_setting_primary_color_title": "Основний колір", "theme_setting_system_primary_color_title": "Використовувати колір системи", "theme_setting_system_theme_switch": "Автоматично (як у системі)", - "theme_setting_theme_subtitle": "Налаштування теми додатка", + "theme_setting_theme_subtitle": "Налаштування теми застосунку", "theme_setting_three_stage_loading_subtitle": "Триетапне завантаження може підвищити продуктивність завантаження, але спричинить значно більше навантаження на мережу", "theme_setting_three_stage_loading_title": "Увімкнути триетапне завантаження", "they_will_be_merged_together": "Вони будуть об'єднані разом", @@ -1941,26 +1991,29 @@ "to_change_password": "Змінити пароль", "to_favorite": "Обране", "to_login": "Вхід", + "to_multi_select": "для багаторазового вибору", "to_parent": "Повернутись назад", - "to_trash": "Смітник", + "to_select": "вибрати", + "to_trash": "Кошик", "toggle_settings": "Перемикання налаштувань", "total": "Усього", "total_usage": "Загальне використання", - "trash": "Смітник", - "trash_action_prompt": "{count} переміщено до смітника", + "trash": "Кошик", + "trash_action_prompt": "{count} переміщено до кошика", "trash_all": "Видалити все", "trash_count": "Видалити {count, number}", - "trash_delete_asset": "Смітник/Видалити ресурс", - "trash_emptied": "Кошик очищений", + "trash_delete_asset": "У кошик/Видалити ресурс", + "trash_emptied": "Кошик очищено", "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", - "trash_page_delete_all": "Видалити усі", + "trash_page_delete_all": "Видалити усе", "trash_page_empty_trash_dialog_content": "Ви хочете очистити кошик? Ці елементи будуть остаточно видалені з Immich", "trash_page_info": "Поміщені у кошик елементи буде остаточно видалено через {days} днів", - "trash_page_no_assets": "Віддалені елементи відсутні", - "trash_page_restore_all": "Відновити усі", - "trash_page_select_assets_btn": "Вибрані елементи", + "trash_page_no_assets": "Видалені елементи відсутні", + "trash_page_restore_all": "Відновити усе", + "trash_page_select_assets_btn": "Вибрати елементи", "trash_page_title": "Кошик ({count})", "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", + "troubleshoot": "Виправлення неполадок", "type": "Тип", "unable_to_change_pin_code": "Неможливо змінити PIN-код", "unable_to_setup_pin_code": "Неможливо налаштувати PIN-код", @@ -1991,6 +2044,7 @@ "unstacked_assets_count": "Розгорнути {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}}", "untagged": "Без тегів", "up_next": "Наступне", + "update_location_action_prompt": "Оновити розташування вибраних об’єктів ({count}) за допомогою:", "updated_at": "Оновлено", "updated_password": "Пароль оновлено", "upload": "Завантажити", @@ -2057,6 +2111,7 @@ "view_next_asset": "Переглянути наступний ресурс", "view_previous_asset": "Переглянути попередній ресурс", "view_qr_code": "Переглянути QR-код", + "view_similar_photos": "Переглянути схожі фотографії", "view_stack": "Перегляд стеку", "view_user": "Переглянути користувача", "viewer_remove_from_stack": "Видалити зі стеку", @@ -2075,5 +2130,6 @@ "yes": "Так", "you_dont_have_any_shared_links": "У вас немає спільних посилань", "your_wifi_name": "Назва вашої Wi-Fi мережі", - "zoom_image": "Збільшити зображення" + "zoom_image": "Збільшити зображення", + "zoom_to_bounds": "Збільшити масштаб до меж" } diff --git a/i18n/vi.json b/i18n/vi.json index 49a73f022c..b9ee8cfcc9 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -360,8 +360,6 @@ "admin_password": "Mật khẩu Quản trị viên", "administration": "Quản trị", "advanced": "Nâng cao", - "advanced_settings_beta_timeline_subtitle": "Trải nghiệm giao diện app mới", - "advanced_settings_beta_timeline_title": "Timeline Beta", "advanced_settings_enable_alternate_media_filter_subtitle": "Dùng tùy chọn này để lọc phương tiện khi đồng bộ theo tiêu chí khác. Chỉ thử khi ứng dụng không nhận diện được tất cả các album.", "advanced_settings_enable_alternate_media_filter_title": "[THỬ NGHIỆM] Dùng bộ lọc đồng bộ album thay thế", "advanced_settings_log_level_title": "Phân loại nhật ký: {level}", @@ -558,8 +556,6 @@ "backup_setting_subtitle": "Quản lý cài đặt tải lên ở chế độ nền và khi đang mở", "backup_settings_subtitle": "Cài đặt việc tải lên", "backward": "Lùi lại", - "beta_sync": "Trạng thái đồng bộ Beta", - "beta_sync_subtitle": "Hệ thống đồng mới", "biometric_auth_enabled": "Đã bật xác thực sinh trắc học", "biometric_locked_out": "Bạn đã bị khóa xác thực bằng sinh trắc học", "biometric_no_options": "Không có tùy chọn bằng sinh trắc học", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 338021de91..fe9108c116 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -28,6 +28,7 @@ "add_to_album": "加入到相簿", "add_to_album_bottom_sheet_added": "新增到 {album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", + "add_to_album_bottom_sheet_some_local_assets": "無法將某些本地資產添加到相册", "add_to_album_toggle": "選擇相簿{album}", "add_to_albums": "加入相簿", "add_to_albums_count": "將({count})個項目加入相簿", @@ -123,6 +124,13 @@ "logging_enable_description": "啟用日誌記錄", "logging_level_description": "啟用時的日誌層級。", "logging_settings": "日誌", + "machine_learning_availability_checks": "可用性檢查", + "machine_learning_availability_checks_description": "自動檢測並優先選擇可用的機器學習服務器", + "machine_learning_availability_checks_enabled": "啟用可用性檢查", + "machine_learning_availability_checks_interval": "檢查間隔", + "machine_learning_availability_checks_interval_description": "可用性檢查之間的間隔(毫秒)", + "machine_learning_availability_checks_timeout": "請求超時", + "machine_learning_availability_checks_timeout_description": "可用性檢查超時(毫秒)", "machine_learning_clip_model": "CLIP 模型", "machine_learning_clip_model_description": "這裡有份 CLIP 模型名單。注意:更換模型後須對所有圖片重新執行「智慧搜尋」任務。", "machine_learning_duplicate_detection": "重複項目偵測", @@ -132,7 +140,7 @@ "machine_learning_enabled": "啟用機器學習", "machine_learning_enabled_description": "若停用,則無視下方的設定,所有機器學習的功能都將停用。", "machine_learning_facial_recognition": "人臉辨識", - "machine_learning_facial_recognition_description": "偵測、辨識並對圖片中的臉孔分組", + "machine_learning_facial_recognition_description": "偵測、辨識並對圖片中的臉孔分類", "machine_learning_facial_recognition_model": "人臉辨識模型", "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。大的模型較慢且使用較多記憶體,但成效較佳。更換模型後需對所有影像重新執行「人臉辨識」。", "machine_learning_facial_recognition_setting": "啟用人臉辨識", @@ -387,8 +395,6 @@ "admin_password": "管理員密碼", "administration": "管理", "advanced": "進階", - "advanced_settings_beta_timeline_subtitle": "試用全新的應用程式體驗", - "advanced_settings_beta_timeline_title": "測試版時間軸", "advanced_settings_enable_alternate_media_filter_subtitle": "使用此選項可在同步時依其他條件篩選媒體。僅在應用程式無法偵測到所有相簿時再嘗試使用。", "advanced_settings_enable_alternate_media_filter_title": "[實驗性] 使用替代的裝置相簿同步篩選器", "advanced_settings_log_level_title": "日誌等級:{level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "偏好遠端影像", "advanced_settings_proxy_headers_subtitle": "定義 Immich 在每次網路請求時應該發送的代理標頭", "advanced_settings_proxy_headers_title": "代理標頭", + "advanced_settings_readonly_mode_subtitle": "開啟唯讀模式後,照片只能瀏覽,像是多選影像、分享、投放、刪除等功能都會關閉。可在主畫面透過使用者頭像來開啟/關閉唯讀模式", + "advanced_settings_readonly_mode_title": "唯讀模式", "advanced_settings_self_signed_ssl_subtitle": "略過伺服器端點的 SSL 憑證驗證。自簽憑證時必須啟用此設定。", "advanced_settings_self_signed_ssl_title": "允許自簽的 SSL 憑證", "advanced_settings_sync_remote_deletions_subtitle": "當在網頁端執行刪除或還原操作時,自動在此裝置上刪除或還原該媒體", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "確定要移除 {user} 嗎?", "album_search_not_found": "找不到符合搜尋條件的相簿", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", + "album_summary": "相册摘要", "album_updated": "更新相簿時", "album_updated_setting_description": "當共享相簿有新項目時用電子郵件通知我", "album_user_left": "離開 {album}", @@ -461,6 +470,7 @@ "app_bar_signout_dialog_title": "登出", "app_settings": "應用程式設定", "appears_in": "出現於", + "apply_count": "應用({count, number})", "archive": "封存", "archive_action_prompt": "已將 ({count}) 個加入進封存", "archive_or_unarchive_photo": "封存或取消封存照片", @@ -493,6 +503,8 @@ "asset_restored_successfully": "媒體復原成功", "asset_skipped": "已跳過", "asset_skipped_in_trash": "已在垃圾桶", + "asset_trashed": "資產被丟棄", + "asset_troubleshoot": "資產故障排除", "asset_uploaded": "已上傳", "asset_uploading": "上傳中…", "asset_viewer_settings_subtitle": "管理您的媒體庫檢視器設定", @@ -500,7 +512,7 @@ "assets": "媒體", "assets_added_count": "已新增 {count, plural, one {# 個媒體} other {# 個媒體}}", "assets_added_to_album_count": "已將 {count, plural, one {# 個媒體} other {# 個媒體}}加入相簿", - "assets_added_to_albums_count": "已新增 {assetTotal, plural, one {# 個} other {# 個}}項目到{albumTotal}個相簿中", + "assets_added_to_albums_count": "已新增 {assetTotal, plural, one {# 個} other {# 個}}項目到 {albumTotal, plural, one {# 個} other {# 個}}相簿中", "assets_cannot_be_added_to_album_count": "無法將 {count, plural, one {媒體} other {媒體}} 加入至相簿", "assets_cannot_be_added_to_albums": "{count, plural, one {個} other {個}}項目無法被加入相簿", "assets_count": "{count, plural, one {# 個媒體} other {# 個媒體}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "自動播放幻燈片", "back": "返回", "back_close_deselect": "返回、關閉及取消選取", + "background_backup_running_error": "後臺備份當前正在運行,無法啟動手動備份", "background_location_permission": "背景存取位置權限", "background_location_permission_content": "為了在背景執行時切換網路,Immich 必須始終具有精確位置存取權限,才能讀取 Wi-Fi 網路名稱", + "background_options": "背景選項", "backup": "備份", "backup_album_selection_page_albums_device": "裝置上的相簿({count})", "backup_album_selection_page_albums_tap": "點一下以選取,點兩下以排除", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "選取相簿", "backup_album_selection_page_selection_info": "選取資訊", "backup_album_selection_page_total_assets": "總不重複媒體數", + "backup_albums_sync": "備份相册同步", "backup_all": "全部", "backup_background_service_backup_failed_message": "備份媒體失敗。正在重試…", "backup_background_service_connection_failed_message": "連線伺服器失敗。正在重試…", @@ -577,7 +592,7 @@ "backup_controller_page_start_backup": "開始備份", "backup_controller_page_status_off": "前台自動備份已關閉", "backup_controller_page_status_on": "前台自動備份已開啟", - "backup_controller_page_storage_format": "{used} 中的 {total} 已使用", + "backup_controller_page_storage_format": "{used} / {total} 已使用", "backup_controller_page_to_backup": "要備份的相簿", "backup_controller_page_total_sub": "已選取相簿中的所有不重複的照片與影片", "backup_controller_page_turn_off": "關閉前台備份", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "管理背景與前台上傳設定", "backup_settings_subtitle": "管理上傳設定", "backward": "由舊至新", - "beta_sync": "測試版同步狀態", - "beta_sync_subtitle": "管理新的同步系統", "biometric_auth_enabled": "生物辨識驗證已啟用", "biometric_locked_out": "您已被鎖定無法使用生物辨識驗證", "biometric_no_options": "沒有生物辨識選項可用", @@ -653,6 +666,8 @@ "change_pin_code": "變更 PIN 碼", "change_your_password": "變更您的密碼", "changed_visibility_successfully": "已成功變更可見性", + "charging": "充電", + "charging_requirement_mobile_backup": "後臺備份要求設備正在充電", "check_corrupt_asset_backup": "檢查損毀的備份項目", "check_corrupt_asset_backup_button": "執行檢查", "check_corrupt_asset_backup_description": "僅在連接 Wi-Fi 且所有媒體已完成備份後執行此檢查。此程序可能需要數分鐘。", @@ -739,6 +754,7 @@ "create_user": "建立使用者", "created": "建立於", "created_at": "建立於", + "creating_linked_albums": "創建連結相册 ...", "crop": "裁剪", "curated_object_page_title": "事物", "current_device": "目前裝置", @@ -888,7 +904,9 @@ "error": "錯誤", "error_change_sort_album": "變更相簿排序失敗", "error_delete_face": "從媒體刪除臉孔時失敗", + "error_getting_places": "獲取位置時出錯", "error_loading_image": "圖片載入錯誤", + "error_loading_partners": "加載合作夥伴時出錯:{error}", "error_saving_image": "錯誤:{error}", "error_tag_face_bounding_box": "標記臉部錯誤 - 無法取得邊界框坐標", "error_title": "錯誤 - 發生錯誤", @@ -1047,117 +1065,121 @@ "failed_to_load_assets": "無法載入媒體", "failed_to_load_folder": "無法載入資料夾", "favorite": "收藏", - "favorite_action_prompt": "已新增 {count} 個到我的最愛", + "favorite_action_prompt": "已新增 {count} 個到收藏", "favorite_or_unfavorite_photo": "收藏或取消收藏照片", "favorites": "收藏", "favorites_page_no_favorites": "未找到收藏項目", "feature_photo_updated": "特色照片已更新", "features": "功能", + "features_in_development": "發展中的特點", "features_setting_description": "管理應用程式功能", - "file_name": "檔名", - "file_name_or_extension": "檔名或副檔名", + "file_name": "檔案名稱", + "file_name_or_extension": "檔案名稱或副檔名", "filename": "檔案名稱", "filetype": "檔案類型", "filter": "濾鏡", "filter_people": "篩選人物", "filter_places": "篩選地點", - "find_them_fast": "搜尋名稱,快速找人", + "find_them_fast": "透過搜尋名稱快速找到他們", "first": "第一個", "fix_incorrect_match": "修復不相符的", "folder": "資料夾", - "folder_not_found": "未找到資料夾", + "folder_not_found": "找不到資料夾", "folders": "資料夾", - "folders_feature_description": "以資料夾瀏覽檔案系統中的照片和影片", - "forgot_pin_code_question": "忘記 PIN 碼?", + "folders_feature_description": "透過資料夾檢視瀏覽檔案系統中的相片與影片", + "forgot_pin_code_question": "忘記您的 PIN 碼?", "forward": "由新至舊", "gcast_enabled": "Google Cast", "gcast_enabled_description": "此功能需要從 Google 載入外部資源才能正常運作。", "general": "一般", + "geolocation_instruction_location": "點擊具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點", "get_help": "線上求助", "get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連接至 Wi-Fi 網路", "getting_started": "開始使用", "go_back": "返回", - "go_to_folder": "轉至資料夾", + "go_to_folder": "前往資料夾", "go_to_search": "前往搜尋", + "gps": "GPS", + "gps_missing": "無GPS", "grant_permission": "授予權限", "group_albums_by": "分類群組的方式...", - "group_country": "按國家分組", - "group_no": "無分組", - "group_owner": "按擁有者分組", + "group_country": "按照國家分類", + "group_no": "沒有分類", + "group_owner": "按擁有者分類", "group_places_by": "分類地點的方式...", - "group_year": "按年份分組", - "haptic_feedback_switch": "啓用振動反饋", - "haptic_feedback_title": "振動反饋", - "has_quota": "配額", - "hash_asset": "雜湊項目", - "hashed_assets": "已雜湊項目", - "hashing": "計算雜湊值", + "group_year": "按年份分類", + "haptic_feedback_switch": "啟用震動回饋", + "haptic_feedback_title": "震動回饋", + "has_quota": "已設定配額", + "hash_asset": "雜湊媒體", + "hashed_assets": "已雜湊的媒體", + "hashing": "正在計算雜湊值", "header_settings_add_header_tip": "新增標頭", - "header_settings_field_validator_msg": "設定不可為空", + "header_settings_field_validator_msg": "值不可為空", "header_settings_header_name_input": "標頭名稱", "header_settings_header_value_input": "標頭值", - "headers_settings_tile_subtitle": "定義代理標頭,套用於每次網絡請求", + "headers_settings_tile_subtitle": "定義應用程式在每次網路請求時應附帶的代理標頭", "headers_settings_tile_title": "自定義代理標頭", "hi_user": "嗨!{name}({email})", "hide_all_people": "隱藏所有人物", - "hide_gallery": "隱藏畫廊", + "hide_gallery": "隱藏媒體庫", "hide_named_person": "隱藏 {name}", "hide_password": "隱藏密碼", "hide_person": "隱藏人物", - "hide_unnamed_people": "隱藏未命名人物", - "home_page_add_to_album_conflicts": "已在相簿 {album} 中新增 {added} 個項目。其中 {failed} 個項目已經在相簿中。", - "home_page_add_to_album_err_local": "暫不能將本地項目新增到相簿中,略過", - "home_page_add_to_album_success": "已在相簿 {album} 中新增 {added} 個項目。", - "home_page_album_err_partner": "暫無法將親朋好友的項目新增到相簿,略過", - "home_page_archive_err_local": "暫無法封存本地項目,略過", - "home_page_archive_err_partner": "無法封存親朋好友的項目,略過", - "home_page_building_timeline": "正在生成時間線", - "home_page_delete_err_partner": "無法刪除親朋好友的項目,略過", - "home_page_delete_remote_err_local": "遙距項目刪除模式,略過本地項目", + "hide_unnamed_people": "隱藏未命名的人物", + "home_page_add_to_album_conflicts": "已將 {added} 個媒體新增到相簿 {album}。{failed} 個媒體已在該相簿中。", + "home_page_add_to_album_err_local": "暫時不能將本地媒體新增到相簿,已略過", + "home_page_add_to_album_success": "已在 {album} 相簿中新增 {added} 個媒體。", + "home_page_album_err_partner": "暫時不能無法將親朋好友的媒體新增到相簿,已略過", + "home_page_archive_err_local": "暫時不能封存本地媒體,已略過", + "home_page_archive_err_partner": "無法封存親朋好友的媒體,已略過", + "home_page_building_timeline": "正在建立時間軸", + "home_page_delete_err_partner": "無法刪除親朋好友的媒體,已略過", + "home_page_delete_remote_err_local": "刪除遠端媒體的選取中包含本機媒體,已略過", "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 個項目,略過", + "home_page_locked_error_partner": "無法移動親朋好友分享的媒體至鎖定的資料夾,已略過", + "home_page_share_err_local": "無法通過連結共享本地媒體,已略過", + "home_page_upload_err_limit": "一次最多只能上傳 30 個媒體,已略過", "host": "主機", "hour": "小時", "hours": "小時", "id": "ID", "idle": "閒置", - "ignore_icloud_photos": "忽略iCloud照片", - "ignore_icloud_photos_description": "存儲在iCloud中的照片不會上傳至Immich伺服器", + "ignore_icloud_photos": "忽略 iCloud 照片", + "ignore_icloud_photos_description": "儲存在 iCloud 中的照片不會上傳至 Immich 伺服器", "image": "圖片", "image_alt_text_date": "{isVideo, select, true {影片} other {圖片}}拍攝於 {date}", "image_alt_text_date_1_person": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 一同於 {date} 拍攝", - "image_alt_text_date_2_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 和 {person2} 一同於 {date} 拍攝", - "image_alt_text_date_3_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", - "image_alt_text_date_place": "{date}在 {country} - {city} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {影片} other {圖片}} 於 {city}、{country},與 {person1} 一同在 {date} 拍攝", - "image_alt_text_date_place_2_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1} 和 {person2} 一同於 {date} 拍攝", - "image_alt_text_date_place_3_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", - "image_saved_successfully": "圖片已儲存", - "image_viewer_page_state_provider_download_started": "下載啓動", + "image_alt_text_date_2_people": "{person1} 和 {person2} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_3_people": "{person1}、{person2} 和 {person3} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_4_or_more_people": "{person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place": "於 {date} 在 {country} - {city} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place_1_person": "在 {country} - {city},與 {person1} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place_2_people": "在 {country} - {city} 與 {person1} 和 {person2} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place_3_people": "在 {country} - {city} 與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place_4_or_more_people": "在 {country} - {city} 與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_saved_successfully": "已儲存圖片", + "image_viewer_page_state_provider_download_started": "下載已啟動", "image_viewer_page_state_provider_download_success": "下載成功", - "image_viewer_page_state_provider_share_error": "共享時發生錯誤", + "image_viewer_page_state_provider_share_error": "分享時發生錯誤", "immich_logo": "Immich 標誌", "immich_web_interface": "Immich 網頁介面", - "import_from_json": "匯入 JSON", + "import_from_json": "從 JSON 匯入", "import_path": "匯入路徑", - "in_albums": "在 {count, plural, other {# 本相簿}}中", - "in_archive": "已封存", + "in_albums": "在 {count, plural, one {# 本相簿} other {# 本相簿}}中", + "in_archive": "在封存中", "include_archived": "包含已封存", "include_shared_albums": "包含共享相簿", - "include_shared_partner_assets": "包括共享親朋好友檔案", + "include_shared_partner_assets": "包括共享親朋好友的媒體", "individual_share": "個別分享", "individual_shares": "個別分享", "info": "資訊", "interval": { "day_at_onepm": "每天下午 1 點", - "hours": "每 {hours, plural, other {{hours, number} 小時}}", + "hours": "每 {hours, plural, one {小時} other {{hours, number} 小時}}", "night_at_midnight": "每晚午夜", "night_at_twoam": "每晚凌晨 2 點" }, @@ -1165,13 +1187,13 @@ "invalid_date_format": "無效的日期格式", "invite_people": "邀請人員", "invite_to_album": "邀請至相簿", - "ios_debug_info_fetch_ran_at": "於{dateTime}執行取回", - "ios_debug_info_last_sync_at": "於{dateTime}最後一次同步", + "ios_debug_info_fetch_ran_at": "抓取已於 {dateTime} 執行", + "ios_debug_info_last_sync_at": "上次同步於 {dateTime}", "ios_debug_info_no_processes_queued": "無排程中的背景程序", "ios_debug_info_no_sync_yet": "尚未執行任何背景同步任務", "ios_debug_info_processes_queued": "{count, plural, one {{count} 個背景程序已排程} other {{count} 個背景程序已排程}}", - "ios_debug_info_processing_ran_at": "程序於{dateTime}執行", - "items_count": "{count, plural, other {# 個項目}}", + "ios_debug_info_processing_ran_at": "於 {dateTime} 執行處理", + "items_count": "{count, plural, one {# 個項目} other {# 個項目}}", "jobs": "任務", "keep": "保留", "keep_all": "全部保留", @@ -1214,6 +1236,7 @@ "local": "本地", "local_asset_cast_failed": "無法轉換未上傳至伺服器的項目", "local_assets": "本地項目", + "local_media_summary": "當地媒體摘要", "local_network": "本地網路", "local_network_sheet_info": "當使用指定的 Wi-Fi 網路時,應用程式將透過此網址連線至伺服器", "location_permission": "位置權限", @@ -1225,6 +1248,7 @@ "location_picker_longitude_hint": "請在此處輸入您的經度值", "lock": "鎖定", "locked_folder": "鎖定的資料夾", + "log_detail_title": "日誌詳細資訊", "log_out": "登出", "log_out_all_devices": "登出所有裝置", "logged_in_as": "以{user}身分登入", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "密碼更新成功", "logout_all_device_confirmation": "您確定要登出所有裝置嗎?", "logout_this_device_confirmation": "要登出這臺裝置嗎?", + "logs": "日誌", "longitude": "經度", "look": "樣貌", "loop_videos": "重播影片", @@ -1262,6 +1287,7 @@ "main_branch_warning": "您現在使用的是開發版本;我們強烈您建議使用正式發行版!", "main_menu": "主頁面", "make": "製造商", + "manage_geolocation": "管理位置", "manage_shared_links": "管理共享連結", "manage_sharing_with_partners": "管理與親朋好友的分享", "manage_the_app_settings": "管理應用程式設定", @@ -1296,6 +1322,7 @@ "mark_as_read": "標記為已讀", "marked_all_as_read": "已全部標記為已讀", "matches": "相符", + "matching_assets": "匹配資產", "media_type": "媒體類型", "memories": "回憶", "memories_all_caught_up": "已全部看完", @@ -1336,6 +1363,7 @@ "name_or_nickname": "名稱或暱稱", "network_requirement_photos_upload": "使用行動網路流量備份照片", "network_requirement_videos_upload": "使用行動網路流量備份影片", + "network_requirements": "網絡要求", "network_requirements_updated": "網絡需求已變更,現重置備份佇列", "networking_settings": "網路", "networking_subtitle": "管理伺服器端點設定", @@ -1346,6 +1374,7 @@ "new_person": "新的人物", "new_pin_code": "新 PIN 碼", "new_pin_code_subtitle": "這是您第一次存取鎖定的資料夾。建立 PIN 碼以安全存取此頁面", + "new_timeline": "新時間軸", "new_user_created": "已建立新使用者", "new_version_available": "新版本已發布", "newest_first": "最新優先", @@ -1359,20 +1388,25 @@ "no_assets_message": "按這裡上傳您的第一張照片", "no_assets_to_show": "無項目展示", "no_cast_devices_found": "沒有找到 Google Cast 裝置", + "no_checksum_local": "沒有可用的校驗和-無法獲取本地資產", + "no_checksum_remote": "沒有可用的校驗和-無法獲取遠程資產", "no_duplicates_found": "沒發現重複項目。", "no_exif_info_available": "沒有可用的 Exif 資訊", "no_explore_results_message": "上傳更多照片以利探索。", "no_favorites_message": "加入收藏,加速尋找影像", "no_libraries_message": "建立外部媒體庫以查看您的照片和影片", + "no_local_assets_found": "未找到具有此校驗和的本地資產", "no_locked_photos_message": "鎖定的資料夾中的照片和影片會被隱藏,當您瀏覽或搜尋圖庫時不會顯示。", "no_name": "無名", "no_notifications": "沒有通知", "no_people_found": "找不到符合的人物", "no_places": "沒有地點", + "no_remote_assets_found": "未找到具有此校驗和的遠程資產", "no_results": "沒有結果", "no_results_description": "試試同義詞或更通用的關鍵字吧", "no_shared_albums_message": "建立相簿分享照片和影片", "no_uploads_in_progress": "沒有正在上傳的項目", + "not_available": "不適用", "not_in_any_album": "不在任何相簿中", "not_selected": "未選擇", "note_apply_storage_label_to_previously_uploaded assets": "*註:執行套用儲存標籤前先上傳項目", @@ -1407,6 +1441,8 @@ "open_the_search_filters": "開啟搜尋篩選器", "options": "選項", "or": "或", + "organize_into_albums": "整理成相册", + "organize_into_albums_description": "使用當前同步設定將現有照片放入相册", "organize_your_library": "整理您的圖庫", "original": "原圖", "other": "其他", @@ -1446,7 +1482,7 @@ "pending": "待處理", "people": "人物", "people_edits_count": "編輯了 {count, plural, one {# 位人士} other {# 位人士}}", - "people_feature_description": "以人物分組瀏覽照片和影片", + "people_feature_description": "以人物分類瀏覽照片和影片", "people_sidebar_description": "在側邊欄顯示「人物」的連結", "permanent_deletion_warning": "永久刪除警告", "permanent_deletion_warning_setting_description": "在永久刪除檔案時顯示警告", @@ -1468,7 +1504,7 @@ "person": "人物", "person_age_months": "{months, plural, one {# 個月} other {# 個月}}前", "person_age_year_months": "1 年 {months, plural, one {# 個月} other {# 個月}}前", - "person_age_years": "{years, plural, other {# 年}}前", + "person_age_years": "{years, plural, other {# 歲}}", "person_birthdate": "生於 {date}", "person_hidden": "{name}{hidden, select, true {(隱藏)} other {}}", "photo_shared_all_users": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", @@ -1492,6 +1528,7 @@ "port": "埠口", "preferences_settings_subtitle": "管理應用程式偏好設定", "preferences_settings_title": "偏好設定", + "preparing": "準備", "preset": "預設", "preview": "預覽", "previous": "上一張", @@ -1508,6 +1545,7 @@ "profile_drawer_client_out_of_date_minor": "客戶端有小版本升級,請盡快升級至最新版。", "profile_drawer_client_server_up_to_date": "客戶端和服務端都是最新的", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "唯讀模式已開啟。請長按使用者頭像圖示以退出。", "profile_drawer_server_out_of_date_major": "服務端有大版本升級,請盡快升級至最新版。", "profile_drawer_server_out_of_date_minor": "服務端有小版本升級,請盡快升級至最新版。", "profile_image_of_user": "{user} 的個人資料圖片", @@ -1546,6 +1584,7 @@ "purchase_server_description_2": "擁護者狀態", "purchase_server_title": "伺服器", "purchase_settings_server_activated": "伺服器產品金鑰是由管理者管理的", + "query_asset_id": "査詢資產ID", "queue_status": "處理中 {count}/{total}", "rating": "評星", "rating_clear": "清除評等", @@ -1553,6 +1592,9 @@ "rating_description": "在資訊面板中顯示 EXIF 評等", "reaction_options": "反應選項", "read_changelog": "閱覽變更日誌", + "readonly_mode_disabled": "唯讀模式已關閉", + "readonly_mode_enabled": "唯讀模式已開啟", + "ready_for_upload": "已準備好上傳", "reassign": "重新指定", "reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個檔案}}重新指定給{name, select, null {現有的人} other {{name}}}", "reassigned_assets_to_new_person": "已將 {count, plural, other {# 個檔案}}重新指定給一位新人物", @@ -1577,6 +1619,7 @@ "regenerating_thumbnails": "重新產生縮圖中", "remote": "遠端", "remote_assets": "遠端項目", + "remote_media_summary": "遠程媒體摘要", "remove": "移除", "remove_assets_album_confirmation": "確定要從相簿中移除 {count, plural, other {# 個檔案}}嗎?", "remove_assets_shared_link_confirmation": "確定刪除共享連結中{count, plural, other {# 個項目}}嗎?", @@ -1629,6 +1672,7 @@ "restore_user": "還原使用者", "restored_asset": "已還原檔案", "resume": "繼續", + "resume_paused_jobs": "恢復 {count, plural, one {# 暫停的任務} other {# 暫停的任務}}", "retry_upload": "重新上傳", "review_duplicates": "檢視重複項目", "review_large_files": "檢視大型文件", @@ -1722,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "新增相簿失敗", "selected": "已選擇", "selected_count": "{count, plural, other {選了 # 項}}", + "selected_gps_coordinates": "選定的GPS座標", "send_message": "傳訊息", "send_welcome_email": "傳送歡迎電子郵件", "server_endpoint": "伺服器端點", @@ -1850,6 +1895,7 @@ "show_slideshow_transition": "顯示幻燈片轉場", "show_supporter_badge": "擁護者徽章", "show_supporter_badge_description": "顯示擁護者徽章", + "show_text_search_menu": "顯示文字蒐索選單", "shuffle": "隨機排序", "sidebar": "側邊欄", "sidebar_display_description": "在側邊欄中顯示鏈結", @@ -1880,6 +1926,7 @@ "stacktrace": "堆疊追蹤", "start": "開始", "start_date": "開始日期", + "start_date_before_end_date": "開始日期必須早於結束日期", "state": "地區", "status": "狀態", "stop_casting": "停止casting", @@ -1904,11 +1951,13 @@ "sync_albums_manual_subtitle": "將所有上傳的短片和照片同步到選定的備份相簿", "sync_local": "同步本機", "sync_remote": "同步遠端", + "sync_status": "同步状态", + "sync_status_subtitle": "查看和管理同步系統", "sync_upload_album_setting_subtitle": "新增照片和短片並上傳到 Immich 上的選定相簿中", "tag": "標籤", "tag_assets": "標記檔案", "tag_created": "已建立標籤:{tag}", - "tag_feature_description": "以邏輯標記要旨分組瀏覽照片和影片", + "tag_feature_description": "以邏輯標記要旨分類瀏覽照片和影片", "tag_not_found_question": "找不到標籤?建立新標籤。", "tag_people": "標籤人物", "tag_updated": "已更新標籤:{tag}", @@ -1941,7 +1990,9 @@ "to_change_password": "變更密碼", "to_favorite": "收藏", "to_login": "登入", + "to_multi_select": "進行多選", "to_parent": "到上一級", + "to_select": "选择", "to_trash": "垃圾桶", "toggle_settings": "切換設定", "total": "統計", @@ -1961,6 +2012,7 @@ "trash_page_select_assets_btn": "選擇項目", "trash_page_title": "垃圾桶 ({count})", "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", + "troubleshoot": "疑难解答", "type": "類型", "unable_to_change_pin_code": "無法變更 PIN 碼", "unable_to_setup_pin_code": "無法設定 PIN 碼", @@ -1991,6 +2043,7 @@ "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", "untagged": "無標籤", "up_next": "下一個", + "update_location_action_prompt": "使用以下命令更新{count}個所選資產的位置:", "updated_at": "更新於", "updated_password": "已更新密碼", "upload": "上傳", @@ -2057,6 +2110,7 @@ "view_next_asset": "查看下一項", "view_previous_asset": "查看上一項", "view_qr_code": "查看 QR code", + "view_similar_photos": "查看相似照片", "view_stack": "查看堆疊", "view_user": "顯示使用者", "viewer_remove_from_stack": "從堆疊中移除", @@ -2075,5 +2129,6 @@ "yes": "是", "you_dont_have_any_shared_links": "您沒有任何共享連結", "your_wifi_name": "您的 Wi-Fi 名稱", - "zoom_image": "縮放圖片" + "zoom_image": "縮放圖片", + "zoom_to_bounds": "縮放到邊界" } diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index 816cbcf29f..1f1455aa19 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -26,8 +26,9 @@ "add_tag": "添加标签", "add_to": "添加到…", "add_to_album": "添加到相册", - "add_to_album_bottom_sheet_added": "添加到 {album}", - "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", + "add_to_album_bottom_sheet_added": "添加到相册 “{album}”", + "add_to_album_bottom_sheet_already_exists": "已在相册“ {album} ” 中", + "add_to_album_bottom_sheet_some_local_assets": "某些本地资产无法添加到相册", "add_to_album_toggle": "选择相册 {album}", "add_to_albums": "添加到相册", "add_to_albums_count": "添加到相册({count}个)", @@ -100,7 +101,7 @@ "image_thumbnail_description": "剥离元数据的小缩略图,用于浏览主时间线等照片组", "image_thumbnail_quality_description": "缩略图质量从 1 到 100。越高越好,但会产生更大的文件,并且会降低系统的响应能力。", "image_thumbnail_title": "缩略图设置", - "job_concurrency": "{job}并发", + "job_concurrency": "{job}任务并发", "job_created": "任务已创建", "job_not_concurrency_safe": "此任务并发并不安全。", "job_settings": "任务设置", @@ -121,13 +122,20 @@ "library_watching_settings": "监控图库(实验性)", "library_watching_settings_description": "自动监控文件变化", "logging_enable_description": "启用日志记录", - "logging_level_description": "启用的日志级别。", + "logging_level_description": "启用时,要使用的日志级别。", "logging_settings": "日志", + "machine_learning_availability_checks": "可用性检查", + "machine_learning_availability_checks_description": "自动检测并优先选择可用的机器学习服务器", + "machine_learning_availability_checks_enabled": "启用可用性检查", + "machine_learning_availability_checks_interval": "检查间隔", + "machine_learning_availability_checks_interval_description": "两次可用性检查之间的间隔(毫秒)", + "machine_learning_availability_checks_timeout": "请求超时", + "machine_learning_availability_checks_timeout_description": "用于可用性检查的超时时间(毫秒)", "machine_learning_clip_model": "CLIP 模型", "machine_learning_clip_model_description": "请于 此处查看支持的 CLIP 模型名称。注意,更换模型后需要对所有图片重新运行“智能搜索”任务。", "machine_learning_duplicate_detection": "重复项检测", "machine_learning_duplicate_detection_enabled": "启用重复检测", - "machine_learning_duplicate_detection_enabled_description": "如果禁用此功能,完全相同的项目仍将被去重。", + "machine_learning_duplicate_detection_enabled_description": "如果禁用,完全相同的项目仍将被去重。", "machine_learning_duplicate_detection_setting_description": "使用 CLIP 向量匹配(关键词相似度)来查找可能的重复项", "machine_learning_enabled": "启用机器学习", "machine_learning_enabled_description": "如果禁用,无论以下如何设置,所有机器学习功能将被禁用。", @@ -158,7 +166,7 @@ "map_enable_description": "启用地图功能", "map_gps_settings": "地图与 GPS 设置", "map_gps_settings_description": "管理地图与 GPS(反向地理编码)设置", - "map_implications": "地图功能依赖于外部地形贴图服务(tiles.immich.cloud)", + "map_implications": "地图功能依赖于外部地图瓦片服务(tiles.immich.cloud)", "map_light_style": "浅色模式", "map_manage_reverse_geocoding_settings": "管理反向地理编码设置", "map_reverse_geocoding": "反向地理编码", @@ -220,7 +228,7 @@ "oauth_enable_description": "使用 OAuth 登录", "oauth_mobile_redirect_uri": "移动端重定向 URI", "oauth_mobile_redirect_uri_override": "移动端重定向 URI 覆盖", - "oauth_mobile_redirect_uri_override_description": "当 OAuth 提供商不允许使用移动 URI 时启用,如“''{callback}''”", + "oauth_mobile_redirect_uri_override_description": "当 OAuth 提供商不允许使用移动 URI 时启用,如“{callback}”", "oauth_role_claim": "角色声明", "oauth_role_claim_description": "根据此声明的存在自动授予管理员访问权限。声明可以是“user”(用户)或“admin”(管理员)。", "oauth_settings": "OAuth", @@ -364,7 +372,7 @@ "user_cleanup_job": "清理用户", "user_delete_delay": "{user}的账户及项目将在{delay, plural, one {#天} other {#天}}后自动永久删除。", "user_delete_delay_settings": "延期删除", - "user_delete_delay_settings_description": "永久删除账户及其所有项目之前所保留的天数。用户删除作业会在午夜检查是否有用户可以删除。对该设置的更改将在下次执行时生效。", + "user_delete_delay_settings_description": "删除后永久删除用户帐户和资产的天数。用户删除作业会在午夜检查是否有用户可以删除。对该设置的更改将在下次执行时生效。", "user_delete_immediately": "{user}的账户及项目将立即永久删除。", "user_delete_immediately_checkbox": "立即删除检索到的用户及项目", "user_details": "用户详情", @@ -387,8 +395,6 @@ "admin_password": "管理员密码", "administration": "系统管理", "advanced": "高级", - "advanced_settings_beta_timeline_subtitle": "体验全新的应用程序", - "advanced_settings_beta_timeline_title": "测试版时间线", "advanced_settings_enable_alternate_media_filter_subtitle": "使用此选项可在同步过程中根据备用条件筛选项目。仅当您在应用程序检测所有相册均遇到问题时才尝试此功能。", "advanced_settings_enable_alternate_media_filter_title": "[实验] 使用备用的设备相册同步筛选条件", "advanced_settings_log_level_title": "日志等级: {level}", @@ -396,6 +402,8 @@ "advanced_settings_prefer_remote_title": "优先远程项目", "advanced_settings_proxy_headers_subtitle": "定义代理标头,应用于 Immich 的每次网络请求", "advanced_settings_proxy_headers_title": "代理标头", + "advanced_settings_readonly_mode_subtitle": "启用只读模式,在该模式下只能查看照片,多选、共享、投屏、删除等操作都被禁用。从主屏幕通过用户头像启用/禁用只读", + "advanced_settings_readonly_mode_title": "只读模式", "advanced_settings_self_signed_ssl_subtitle": "跳过对服务器 的 SSL 证书验证(该选项适用于使用自签名证书的服务器)。", "advanced_settings_self_signed_ssl_title": "允许自签名 SSL 证书", "advanced_settings_sync_remote_deletions_subtitle": "在网页上执行操作时,自动删除或还原该设备中的项目", @@ -423,6 +431,7 @@ "album_remove_user_confirmation": "确定要移除“{user}”吗?", "album_search_not_found": "未找到符合搜索条件的相册", "album_share_no_users": "看起来您已与所有用户共享了此相册,或者您根本没有任何用户可共享。", + "album_summary": "相册摘要", "album_updated": "相册有更新", "album_updated_setting_description": "当共享相册有新项目时接收邮件通知", "album_user_left": "离开“{album}”", @@ -456,11 +465,12 @@ "api_key_description": "该应用密钥只会显示一次。请确保在关闭窗口前复制下来。", "api_key_empty": "API 密钥名称不可为空", "api_keys": "API 密钥", - "app_bar_signout_dialog_content": "确定退出登录?", + "app_bar_signout_dialog_content": "是否确定退出登录?", "app_bar_signout_dialog_ok": "是", "app_bar_signout_dialog_title": "退出登录", "app_settings": "应用设置", "appears_in": "出现于", + "apply_count": "应用 ({count, number}个资产)", "archive": "归档", "archive_action_prompt": "已将 {count} 项添加到归档", "archive_or_unarchive_photo": "归档或取消归档照片", @@ -493,6 +503,8 @@ "asset_restored_successfully": "已成功恢复所有项目", "asset_skipped": "已跳过", "asset_skipped_in_trash": "已回收", + "asset_trashed": "资产已被删除", + "asset_troubleshoot": "资产故障排除", "asset_uploaded": "已上传", "asset_uploading": "上传中…", "asset_viewer_settings_subtitle": "管理图库浏览器设置", @@ -500,7 +512,7 @@ "assets": "项目", "assets_added_count": "已添加{count, plural, one {#个项目} other {#个项目}}", "assets_added_to_album_count": "已添加{count, plural, one {#个项目} other {#个项目}}到相册", - "assets_added_to_albums_count": "已添加 {assetTotal, plural, one {# 个项目} other {# 个项目}}到 {albumTotal} 个相册", + "assets_added_to_albums_count": "已添加 {assetTotal, plural, one {# 个项目} other {# 个项目}}到 {albumTotal, plural, one {# 个相册} other {# 个相册}}", "assets_cannot_be_added_to_album_count": "无法添加 {count, plural, one {个项目} other {个项目}} 到相册中", "assets_cannot_be_added_to_albums": "无法添加 {count, plural, one {个项目} other {个项目}} 到相册", "assets_count": "{count, plural, one {#个项目} other {#个项目}}", @@ -526,8 +538,10 @@ "autoplay_slideshow": "自动播放幻灯片", "back": "返回", "back_close_deselect": "返回、关闭或反选", + "background_backup_running_error": "后台备份正在运行,无法启动手动备份", "background_location_permission": "后台定位权限", "background_location_permission_content": "为确保后台运行时自动切换网络,需授予 Immich *始终允许精确定位* 权限,以识别 Wi-Fi 网络名称", + "background_options": "背景选项", "backup": "备份", "backup_album_selection_page_albums_device": "设备上的相册({count})", "backup_album_selection_page_albums_tap": "单击选中,双击取消", @@ -535,6 +549,7 @@ "backup_album_selection_page_select_albums": "选择相册", "backup_album_selection_page_selection_info": "选择信息", "backup_album_selection_page_total_assets": "总计", + "backup_albums_sync": "备份相册同步", "backup_all": "全部", "backup_background_service_backup_failed_message": "备份失败,正在重试…", "backup_background_service_connection_failed_message": "连接服务器失败,正在重试…", @@ -594,8 +609,6 @@ "backup_setting_subtitle": "管理后台和前台上传设置", "backup_settings_subtitle": "管理上传设置", "backward": "后退", - "beta_sync": "测试版同步状态", - "beta_sync_subtitle": "管理新的同步系统", "biometric_auth_enabled": "生物识别身份验证已启用", "biometric_locked_out": "您被锁定在生物识别身份验证之外", "biometric_no_options": "没有可用的生物识别选项", @@ -653,6 +666,8 @@ "change_pin_code": "修改PIN码", "change_your_password": "修改您的密码", "changed_visibility_successfully": "更改可见性成功", + "charging": "充电", + "charging_requirement_mobile_backup": "后台备份需要设备处于充电状态", "check_corrupt_asset_backup": "检查备份是否损坏", "check_corrupt_asset_backup_button": "执行检查", "check_corrupt_asset_backup_description": "仅在连接到 Wi-Fi 并完成所有项目备份后执行此检查。该过程可能需要几分钟。", @@ -739,6 +754,7 @@ "create_user": "创建用户", "created": "已创建", "created_at": "已创建", + "creating_linked_albums": "正在创建相册链接…", "crop": "裁剪", "curated_object_page_title": "事物", "current_device": "当前设备", @@ -888,14 +904,16 @@ "error": "错误", "error_change_sort_album": "更改相册排序失败", "error_delete_face": "删除人脸失败", + "error_getting_places": "获取位置时出错", "error_loading_image": "加载图片时出错", + "error_loading_partners": "加载同伴时出错:{error}", "error_saving_image": "错误:{error}", "error_tag_face_bounding_box": "标记人脸出错 - 无法获取人脸框坐标", "error_title": "错误 - 好像出了问题", "errors": { "cannot_navigate_next_asset": "无法导航到下一个项目", "cannot_navigate_previous_asset": "无法导航到上一个项目", - "cant_apply_changes": "无用应用更改", + "cant_apply_changes": "无法应用更改", "cant_change_activity": "无法{enabled, select, true {禁用} other {启用}}活动", "cant_change_asset_favorite": "无法修改项目的收藏属性", "cant_change_metadata_assets_count": "无法修改{count, plural, one {#个项目} other {#个项目}}的元数据", @@ -1053,6 +1071,7 @@ "favorites_page_no_favorites": "未找到收藏项目", "feature_photo_updated": "人物头像已更新", "features": "功能", + "features_in_development": "开发中的功能", "features_setting_description": "管理 App 功能", "file_name": "文件名", "file_name_or_extension": "文件名", @@ -1073,12 +1092,15 @@ "gcast_enabled": "Google Cast 投屏", "gcast_enabled_description": "该功能需要加载来自 Google 的外部资源。", "general": "通用", + "geolocation_instruction_location": "点击带有GPS坐标的资产以使用其位置,或直接从地图上选择位置", "get_help": "获取帮助", "get_wifiname_error": "无法获取 Wi-Fi 名称。确保已授予必要的权限,并已连接到 Wi-Fi 网络", "getting_started": "入门", "go_back": "返回", "go_to_folder": "进入文件夹", "go_to_search": "前往搜索", + "gps": "有GPS信息", + "gps_missing": "无GPS信息", "grant_permission": "获取权限", "group_albums_by": "相册分组依据...", "group_country": "按国家分组", @@ -1214,17 +1236,19 @@ "local": "本地", "local_asset_cast_failed": "无法投放未上传至服务器的项目", "local_assets": "本地项目", + "local_media_summary": "本地媒体摘要", "local_network": "本地网络", "local_network_sheet_info": "当使用指定的 Wi-Fi 网络时,应用程序将通过此 URL 访问服务器", "location_permission": "定位权限", "location_permission_content": "使用自动切换功能,Immich 需要精确定位权限,以获取当前 Wi-Fi 网络名称", - "location_picker_choose_on_map": "在地图上选择", - "location_picker_latitude_error": "输入有效的纬度值", - "location_picker_latitude_hint": "请在此处输入您的纬度值", - "location_picker_longitude_error": "输入有效的经度值", - "location_picker_longitude_hint": "请在此处输入您的经度值", + "location_picker_choose_on_map": "在地图上定位", + "location_picker_latitude_error": "请输入有效的纬度", + "location_picker_latitude_hint": "请在此处输入纬度", + "location_picker_longitude_error": "请输入有效的经度", + "location_picker_longitude_hint": "请在此处输入经度", "lock": "锁定", "locked_folder": "锁定文件夹", + "log_detail_title": "日志详细信息", "log_out": "注销", "log_out_all_devices": "注销所有设备", "logged_in_as": "以 {user} 身份登录", @@ -1255,6 +1279,7 @@ "login_password_changed_success": "密码更新成功", "logout_all_device_confirmation": "确定要从所有设备注销?", "logout_this_device_confirmation": "确定要从本设备注销?", + "logs": "日志", "longitude": "经度", "look": "样式", "loop_videos": "循环视频", @@ -1262,6 +1287,7 @@ "main_branch_warning": "您当前使用的是开发版;我们强烈建议您使用正式发行版(release版)!", "main_menu": "主菜单", "make": "品牌", + "manage_geolocation": "管理坐标位置", "manage_shared_links": "管理共享链接", "manage_sharing_with_partners": "管理与同伴的共享", "manage_the_app_settings": "管理应用设置", @@ -1288,7 +1314,7 @@ "map_settings_date_range_option_years": "{years} 年前", "map_settings_dialog_title": "地图设置", "map_settings_include_show_archived": "包括已归档项目", - "map_settings_include_show_partners": "包含伙伴", + "map_settings_include_show_partners": "包含同伴", "map_settings_only_show_favorites": "仅显示收藏的项目", "map_settings_theme_settings": "地图主题", "map_zoom_to_see_photos": "缩小以查看项目", @@ -1296,6 +1322,7 @@ "mark_as_read": "标记为已读", "marked_all_as_read": "已全部标记为已读", "matches": "匹配", + "matching_assets": "匹配资产", "media_type": "媒体类型", "memories": "回忆", "memories_all_caught_up": "已全部看完", @@ -1336,6 +1363,7 @@ "name_or_nickname": "名称或昵称", "network_requirement_photos_upload": "使用蜂窝数据备份照片", "network_requirement_videos_upload": "使用蜂窝数据备份视频", + "network_requirements": "网络要求", "network_requirements_updated": "网络要求发生变化,正在重置备份队列", "networking_settings": "网络", "networking_subtitle": "管理服务器接口设置", @@ -1346,6 +1374,7 @@ "new_person": "新人物", "new_pin_code": "新的PIN码", "new_pin_code_subtitle": "这是您第一次访问此锁定文件夹。创建一个PIN码以安全访问此页面", + "new_timeline": "新建时间轴", "new_user_created": "已创建新用户", "new_version_available": "有新版本发布啦", "newest_first": "最新优先", @@ -1357,22 +1386,27 @@ "no_albums_yet": "貌似您还没有创建相册。", "no_archived_assets_message": "归档照片和视频以便在照片视图中隐藏它们", "no_assets_message": "点击上传您的第一张照片", - "no_assets_to_show": "无项目展示", + "no_assets_to_show": "没有要显示的资产", "no_cast_devices_found": "未找到投放设备", + "no_checksum_local": "没有可用的校验和-无法获取本地资产", + "no_checksum_remote": "没有可用的校验和-无法获取远程资产", "no_duplicates_found": "未发现重复项。", "no_exif_info_available": "没有可用的 EXIF 信息", "no_explore_results_message": "上传更多照片来探索。", "no_favorites_message": "添加到收藏夹,快速查找最佳图片和视频", "no_libraries_message": "创建外部图库来查看您的照片和视频", + "no_local_assets_found": "未找到具有此校验和的本地资产", "no_locked_photos_message": "锁定文件夹中的照片和视频将被隐藏,不会在您浏览、搜索图库时出现。", "no_name": "未命名", "no_notifications": "没有通知", "no_people_found": "未找到匹配的人物", "no_places": "无位置", + "no_remote_assets_found": "未找到具有此校验和的远程资产", "no_results": "无结果", "no_results_description": "尝试使用同义词或更通用的关键词", "no_shared_albums_message": "创建相册以共享照片和视频", "no_uploads_in_progress": "没有正在进行的上传", + "not_available": "不适用", "not_in_any_album": "不在任何相册中", "not_selected": "未选择", "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,需要运行", @@ -1407,6 +1441,8 @@ "open_the_search_filters": "打开搜索过滤器", "options": "选项", "or": "或", + "organize_into_albums": "整理成相册", + "organize_into_albums_description": "使用当前同步设置将现有照片放入相册", "organize_your_library": "整理您的图库", "original": "原图", "other": "其它", @@ -1492,6 +1528,7 @@ "port": "端口", "preferences_settings_subtitle": "管理应用的偏好设置", "preferences_settings_title": "偏好设置", + "preparing": "准备中", "preset": "预设", "preview": "预览", "previous": "上一个", @@ -1508,6 +1545,7 @@ "profile_drawer_client_out_of_date_minor": "客户端有小版本升级,请尽快升级至最新版。", "profile_drawer_client_server_up_to_date": "客户端和服务端都是最新的", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "只读模式已启用。长按用户头像图标退出。", "profile_drawer_server_out_of_date_major": "服务端有大版本升级,请尽快升级至最新版。", "profile_drawer_server_out_of_date_minor": "服务端有小版本升级,请尽快升级至最新版。", "profile_image_of_user": "{user}的个人资料图片", @@ -1546,6 +1584,7 @@ "purchase_server_description_2": "支持者状态", "purchase_server_title": "服务器", "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", + "query_asset_id": "查询资产ID", "queue_status": "排队中 {count}/{total}", "rating": "星级", "rating_clear": "删除星级", @@ -1553,6 +1592,9 @@ "rating_description": "在信息面板中展示 EXIF 星级", "reaction_options": "回应选项", "read_changelog": "阅读更新日志", + "readonly_mode_disabled": "只读模式已禁用", + "readonly_mode_enabled": "只读模式已启用", + "ready_for_upload": "准备上传", "reassign": "重新指派", "reassigned_assets_to_existing_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到{name, select, null {已存在的人物} other {{name}}}", "reassigned_assets_to_new_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到新的人物", @@ -1577,6 +1619,7 @@ "regenerating_thumbnails": "正在重新生成缩略图", "remote": "远程", "remote_assets": "远程项目", + "remote_media_summary": "远程媒体摘要", "remove": "移除", "remove_assets_album_confirmation": "确定要从图库中移除{count, plural, one {#个项目} other {#个项目}}?", "remove_assets_shared_link_confirmation": "确定要从共享链接中移除{count, plural, one {#个项目} other {#个项目}}?", @@ -1629,6 +1672,7 @@ "restore_user": "恢复用户", "restored_asset": "已恢复项目", "resume": "继续", + "resume_paused_jobs": "继续 {count, plural, one {# 已暂停的任务} other {# 已暂停的任务}}", "retry_upload": "重新上传", "review_duplicates": "检查重复项", "review_large_files": "查看大文件", @@ -1722,6 +1766,7 @@ "select_user_for_sharing_page_err_album": "创建相册失败", "selected": "已选择", "selected_count": "{count, plural, other {#项已选择}}", + "selected_gps_coordinates": "已选定的GPS坐标", "send_message": "发送消息", "send_welcome_email": "发送欢迎邮件", "server_endpoint": "服务器 URL", @@ -1850,10 +1895,11 @@ "show_slideshow_transition": "显示幻灯片过渡效果", "show_supporter_badge": "支持者徽章", "show_supporter_badge_description": "展示支持者徽章", + "show_text_search_menu": "显示文本搜索菜单", "shuffle": "随机", "sidebar": "侧边栏", "sidebar_display_description": "在侧边栏中显示链接", - "sign_out": "注销", + "sign_out": "退出登录", "sign_up": "注册", "size": "大小", "skip_to_content": "跳转到内容", @@ -1880,6 +1926,7 @@ "stacktrace": "堆栈跟踪", "start": "开始", "start_date": "开始日期", + "start_date_before_end_date": "开始日期必须在结束日期之前", "state": "省份", "status": "状态", "stop_casting": "停止投放", @@ -1904,6 +1951,8 @@ "sync_albums_manual_subtitle": "将所有上传的视频和照片同步到选定的备份相册", "sync_local": "同步本地", "sync_remote": "同步远程", + "sync_status": "同步状态", + "sync_status_subtitle": "查看和管理同步系统", "sync_upload_album_setting_subtitle": "创建照片和视频并上传到 Immich 上的选定相册中", "tag": "标签", "tag_assets": "标记项目", @@ -1914,7 +1963,7 @@ "tag_updated": "已更新标签:{tag}", "tagged_assets": "{count, plural, one {# 个项目} other {# 个项目}}被加上标签", "tags": "标签", - "tap_to_run_job": "点击运行作业", + "tap_to_run_job": "点击运行任务", "template": "模版", "theme": "主题", "theme_selection": "主题选项", @@ -1941,7 +1990,9 @@ "to_change_password": "修改密码", "to_favorite": "收藏", "to_login": "登录", + "to_multi_select": "多选", "to_parent": "返回上一级", + "to_select": "选择", "to_trash": "放入回收站", "toggle_settings": "切换设置", "total": "总计", @@ -1961,6 +2012,7 @@ "trash_page_select_assets_btn": "选择项目", "trash_page_title": "回收站 ({count})", "trashed_items_will_be_permanently_deleted_after": "回收站中的项目将在{days, plural, one {#天} other {#天}}后被永久删除。", + "troubleshoot": "故障排除", "type": "类型", "unable_to_change_pin_code": "无法修改PIN码", "unable_to_setup_pin_code": "无法设置PIN码", @@ -1991,6 +2043,7 @@ "unstacked_assets_count": "{count, plural, one {#个项目} other {#个项目}}已取消堆叠", "untagged": "无标签", "up_next": "下一个", + "update_location_action_prompt": "更新 {count} 个所选资产的位置:", "updated_at": "已更新", "updated_password": "更新密码", "upload": "上传", @@ -2057,6 +2110,7 @@ "view_next_asset": "查看下一项", "view_previous_asset": "查看上一项", "view_qr_code": "查看二维码", + "view_similar_photos": "查看相似照片", "view_stack": "查看堆叠项目", "view_user": "查看用户", "viewer_remove_from_stack": "从堆叠中移除", @@ -2075,5 +2129,6 @@ "yes": "是", "you_dont_have_any_shared_links": "您没有任何共享链接", "your_wifi_name": "您的 Wi-Fi 名称", - "zoom_image": "缩放图像" + "zoom_image": "缩放图像", + "zoom_to_bounds": "缩放到边界" } diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 7232a25e9e..e4ed643375 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:c642d5dfaf9115a12086785f23008558ae2e13bcd0c4794536340bcb777a4381 AS builder-cpu +FROM python:3.11-bookworm@sha256:fc1f2e357c307c4044133952b203e66a47e7726821a664f603a180a0c5823844 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -22,7 +22,7 @@ FROM builder-cpu AS builder-rknn # Warning: 25GiB+ disk space required to pull this image # TODO: find a way to reduce the image size -FROM rocm/dev-ubuntu-22.04:6.3.4-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm +FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm # renovate: datasource=github-releases depName=Microsoft/onnxruntime ARG ONNXRUNTIME_VERSION="v1.20.1" @@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ RUN apt-get update && apt-get install -y --no-install-recommends g++ -COPY --from=ghcr.io/astral-sh/uv:latest@sha256:f64ad69940b634e75d2e4d799eb5238066c5eeda49f76e782d4873c3d014ea33 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.8.15@sha256:a5727064a0de127bdb7c9d3c1383f3a9ac307d9f2d8a391edc7896c54289ced0 /uv /uvx /bin/ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ @@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \ uv pip install /opt/onnxruntime_rocm-*.whl; \ fi -FROM python:3.11-slim-bookworm@sha256:838ff46ae6c481e85e369706fa3dea5166953824124735639f3c9f52af85f319 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 -FROM python:3.11-slim-bookworm@sha256:838ff46ae6c481e85e369706fa3dea5166953824124735639f3c9f52af85f319 AS prod-openvino +FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ @@ -99,7 +99,7 @@ COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3 COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11 COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so -FROM rocm/dev-ubuntu-22.04:6.3.4-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS prod-rocm +FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS prod-rocm FROM prod-cpu AS prod-armnn diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index f0f08b20b6..93e8bb3898 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "1.129.0" +version = "2.0.0" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] requires-python = ">=3.10,<4.0" diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index db34b66301..c30120a40b 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -507,61 +507,87 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.4" +version = "7.10.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716, upload-time = "2024-10-20T22:57:39.682Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/93/4ad92f71e28ece5c0326e5f4a6630aa4928a8846654a65cfff69b49b95b9/coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", size = 206713, upload-time = "2024-10-20T22:56:03.877Z" }, - { url = "https://files.pythonhosted.org/packages/01/ae/747a580b1eda3f2e431d87de48f0604bd7bc92e52a1a95185a4aa585bc47/coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", size = 207149, upload-time = "2024-10-20T22:56:06.511Z" }, - { url = "https://files.pythonhosted.org/packages/07/1a/1f573f8a6145f6d4c9130bbc120e0024daf1b24cf2a78d7393fa6eb6aba7/coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", size = 235584, upload-time = "2024-10-20T22:56:07.678Z" }, - { url = "https://files.pythonhosted.org/packages/40/42/c8523f2e4db34aa9389caee0d3688b6ada7a84fcc782e943a868a7f302bd/coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", size = 233486, upload-time = "2024-10-20T22:56:09.496Z" }, - { url = "https://files.pythonhosted.org/packages/8d/95/565c310fffa16ede1a042e9ea1ca3962af0d8eb5543bc72df6b91dc0c3d5/coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", size = 234649, upload-time = "2024-10-20T22:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/3b550674d98968ec29c92e3e8650682be6c8b1fa7581a059e7e12e74c431/coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", size = 233744, upload-time = "2024-10-20T22:56:12.481Z" }, - { url = "https://files.pythonhosted.org/packages/0d/70/d66c7f51b3e33aabc5ea9f9624c1c9d9655472962270eb5e7b0d32707224/coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", size = 232204, upload-time = "2024-10-20T22:56:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/23/2d/2b3a2dbed7a5f40693404c8a09e779d7c1a5fbed089d3e7224c002129ec8/coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", size = 233335, upload-time = "2024-10-20T22:56:15.521Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4f/92d1d2ad720d698a4e71c176eacf531bfb8e0721d5ad560556f2c484a513/coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", size = 209435, upload-time = "2024-10-20T22:56:17.309Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/cdf158e7991e2287bcf9082670928badb73d310047facac203ff8dcd5ff3/coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", size = 210243, upload-time = "2024-10-20T22:56:18.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819, upload-time = "2024-10-20T22:56:20.132Z" }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263, upload-time = "2024-10-20T22:56:21.88Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205, upload-time = "2024-10-20T22:56:23.03Z" }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612, upload-time = "2024-10-20T22:56:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479, upload-time = "2024-10-20T22:56:26.749Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405, upload-time = "2024-10-20T22:56:27.958Z" }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038, upload-time = "2024-10-20T22:56:29.816Z" }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812, upload-time = "2024-10-20T22:56:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400, upload-time = "2024-10-20T22:56:33.569Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243, upload-time = "2024-10-20T22:56:34.863Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013, upload-time = "2024-10-20T22:56:36.034Z" }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251, upload-time = "2024-10-20T22:56:38.054Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268, upload-time = "2024-10-20T22:56:40.051Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298, upload-time = "2024-10-20T22:56:41.929Z" }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367, upload-time = "2024-10-20T22:56:43.141Z" }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853, upload-time = "2024-10-20T22:56:44.33Z" }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160, upload-time = "2024-10-20T22:56:46.258Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824, upload-time = "2024-10-20T22:56:48.666Z" }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639, upload-time = "2024-10-20T22:56:50.664Z" }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428, upload-time = "2024-10-20T22:56:52.468Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039, upload-time = "2024-10-20T22:56:53.656Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298, upload-time = "2024-10-20T22:56:54.979Z" }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813, upload-time = "2024-10-20T22:56:56.209Z" }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959, upload-time = "2024-10-20T22:56:58.06Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950, upload-time = "2024-10-20T22:56:59.329Z" }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610, upload-time = "2024-10-20T22:57:00.645Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697, upload-time = "2024-10-20T22:57:01.944Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541, upload-time = "2024-10-20T22:57:03.848Z" }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707, upload-time = "2024-10-20T22:57:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439, upload-time = "2024-10-20T22:57:06.35Z" }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784, upload-time = "2024-10-20T22:57:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058, upload-time = "2024-10-20T22:57:09.845Z" }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772, upload-time = "2024-10-20T22:57:11.147Z" }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490, upload-time = "2024-10-20T22:57:13.02Z" }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848, upload-time = "2024-10-20T22:57:14.927Z" }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340, upload-time = "2024-10-20T22:57:16.246Z" }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229, upload-time = "2024-10-20T22:57:17.546Z" }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510, upload-time = "2024-10-20T22:57:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353, upload-time = "2024-10-20T22:57:20.891Z" }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502, upload-time = "2024-10-20T22:57:22.21Z" }, - { url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954, upload-time = "2024-10-20T22:57:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, + { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, + { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" }, + { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" }, + { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, + { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, + { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, + { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] [package.optional-dependencies] @@ -1341,7 +1367,7 @@ wheels = [ [[package]] name = "locust" -version = "2.38.1" +version = "2.40.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1353,17 +1379,20 @@ dependencies = [ { name = "locust-cloud" }, { name = "msgpack" }, { name = "psutil" }, + { name = "pytest" }, + { name = "python-engineio" }, + { name = "python-socketio", extra = ["client"] }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pyzmq" }, { name = "requests" }, { name = "setuptools" }, { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/03/2f92b75d971e6043cca6fcec59ceccfa800a1324425a74950603d8cac33a/locust-2.38.1.tar.gz", hash = "sha256:4ad9f2f9e7d56b7747ba67cb16e47ca0466b3908f402f50660f15f37621a5218", size = 1406572, upload-time = "2025-08-12T11:38:52.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e0/a99401e233ad1b9ad26265ad8f45f2466abb6ef954e7747e8484864eb6df/locust-2.40.2.tar.gz", hash = "sha256:9ffdf900d1ad949d4c5809e2a4e526bba582175f025f24da2755f43f4b5cb23e", size = 1411854, upload-time = "2025-09-08T12:55:28.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/f6/4a8087f44abd67bb8cc51fba52dcfdddc09d69c154819d56b7da4c79f9ad/locust-2.38.1-py3-none-any.whl", hash = "sha256:34978219ee0d682a135fd4c67f287c26725e7b3fa83d34d65be70efdb42ab4d1", size = 1424130, upload-time = "2025-08-12T11:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/85ddb125d91b3a2bfa2a52eeae2d4c7da062239aaa475d6aebddb5688f41/locust-2.40.2-py3-none-any.whl", hash = "sha256:c8f0060d2bd8479034e9e61e6473669c4c8216930d99ee61ec0e627340b89d3e", size = 1430483, upload-time = "2025-09-08T12:55:25.659Z" }, ] [[package]] @@ -1838,79 +1867,79 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.2" +version = "3.11.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739, upload-time = "2025-08-12T15:12:28.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/7b/7aebe925c6b1c46c8606a960fe1d6b681fccd4aaf3f37cd647c3309d6582/orjson-3.11.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6b8a78c33496230a60dc9487118c284c15ebdf6724386057239641e1eb69761", size = 226896, upload-time = "2025-08-12T15:10:22.02Z" }, - { url = "https://files.pythonhosted.org/packages/7d/39/c952c9b0d51063e808117dd1e53668a2e4325cc63cfe7df453d853ee8680/orjson-3.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc04036eeae11ad4180d1f7b5faddb5dab1dee49ecd147cd431523869514873b", size = 111845, upload-time = "2025-08-12T15:10:24.963Z" }, - { url = "https://files.pythonhosted.org/packages/f5/dc/90b7f29be38745eeacc30903b693f29fcc1097db0c2a19a71ffb3e9f2a5f/orjson-3.11.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c04325839c5754c253ff301cee8aaed7442d974860a44447bb3be785c411c27", size = 116395, upload-time = "2025-08-12T15:10:26.314Z" }, - { url = "https://files.pythonhosted.org/packages/10/c2/fe84ba63164c22932b8d59b8810e2e58590105293a259e6dd1bfaf3422c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32769e04cd7fdc4a59854376211145a1bbbc0aea5e9d6c9755d3d3c301d7c0df", size = 118768, upload-time = "2025-08-12T15:10:27.605Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ce/d9748ec69b1a4c29b8e2bab8233e8c41c583c69f515b373f1fb00247d8c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff285d14917ea1408a821786e3677c5261fa6095277410409c694b8e7720ae0", size = 120887, upload-time = "2025-08-12T15:10:29.153Z" }, - { url = "https://files.pythonhosted.org/packages/c1/66/b90fac8e4a76e83f981912d7f9524d402b31f6c1b8bff3e498aa321c326c/orjson-3.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2662f908114864b63ff75ffe6ffacf996418dd6cc25e02a72ad4bda81b1ec45a", size = 123650, upload-time = "2025-08-12T15:10:30.602Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/56143898d1689c7f915ac67703efb97e8f2f8d5805ce8c2c3fd0f2bb6e3d/orjson-3.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab463cf5d08ad6623a4dac1badd20e88a5eb4b840050c4812c782e3149fe2334", size = 121287, upload-time = "2025-08-12T15:10:31.868Z" }, - { url = "https://files.pythonhosted.org/packages/80/de/f9c6d00c127be766a3739d0d85b52a7c941e437d8dd4d573e03e98d0f89c/orjson-3.11.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:64414241bde943cbf3c00d45fcb5223dca6d9210148ba984aae6b5d63294502b", size = 119637, upload-time = "2025-08-12T15:10:33.078Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/ab70c7627022d395c1b4eb5badf6196b7144e82b46a3a17ed2354f9e592d/orjson-3.11.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7773e71c0ae8c9660192ff144a3d69df89725325e3d0b6a6bb2c50e5ebaf9b84", size = 392478, upload-time = "2025-08-12T15:10:34.669Z" }, - { url = "https://files.pythonhosted.org/packages/77/91/d890b873b69311db4fae2624c5603c437df9c857fb061e97706dac550a77/orjson-3.11.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:652ca14e283b13ece35bf3a86503c25592f294dbcfc5bb91b20a9c9a62a3d4be", size = 134343, upload-time = "2025-08-12T15:10:35.978Z" }, - { url = "https://files.pythonhosted.org/packages/47/16/1aa248541b4830274a079c4aeb2aa5d1ff17c3f013b1d0d8d16d0848f3de/orjson-3.11.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:26e99e98df8990ecfe3772bbdd7361f602149715c2cbc82e61af89bfad9528a4", size = 123887, upload-time = "2025-08-12T15:10:37.601Z" }, - { url = "https://files.pythonhosted.org/packages/95/e4/7419833c55ac8b5f385d00c02685a260da1f391e900fc5c3e0b797e0d506/orjson-3.11.2-cp310-cp310-win32.whl", hash = "sha256:5814313b3e75a2be7fe6c7958201c16c4560e21a813dbad25920752cecd6ad66", size = 124560, upload-time = "2025-08-12T15:10:38.966Z" }, - { url = "https://files.pythonhosted.org/packages/74/f8/27ca7ef3e194c462af32ce1883187f5ec483650c559166f0de59c4c2c5f0/orjson-3.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc471ce2225ab4c42ca672f70600d46a8b8e28e8d4e536088c1ccdb1d22b35ce", size = 119700, upload-time = "2025-08-12T15:10:40.911Z" }, - { url = "https://files.pythonhosted.org/packages/78/7d/e295df1ac9920cbb19fb4c1afa800e86f175cb657143aa422337270a4782/orjson-3.11.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:888b64ef7eaeeff63f773881929434a5834a6a140a63ad45183d59287f07fc6a", size = 226502, upload-time = "2025-08-12T15:10:42.284Z" }, - { url = "https://files.pythonhosted.org/packages/65/21/ffb0f10ea04caf418fb4e7ad1fda4b9ab3179df9d7a33b69420f191aadd5/orjson-3.11.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:83387cc8b26c9fa0ae34d1ea8861a7ae6cff8fb3e346ab53e987d085315a728e", size = 115999, upload-time = "2025-08-12T15:10:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/d5/8da1e252ac3353d92e6f754ee0c85027c8a2cda90b6899da2be0df3ef83d/orjson-3.11.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e35f003692c216d7ee901b6b916b5734d6fc4180fcaa44c52081f974c08e17", size = 111563, upload-time = "2025-08-12T15:10:45.301Z" }, - { url = "https://files.pythonhosted.org/packages/4f/81/baabc32e52c570b0e4e1044b1bd2ccbec965e0de3ba2c13082255efa2006/orjson-3.11.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a0a4c29ae90b11d0c00bcc31533854d89f77bde2649ec602f512a7e16e00640", size = 116222, upload-time = "2025-08-12T15:10:46.92Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/da2ad55ad80b49b560dce894c961477d0e76811ee6e614b301de9f2f8728/orjson-3.11.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:585d712b1880f68370108bc5534a257b561672d1592fae54938738fe7f6f1e33", size = 118594, upload-time = "2025-08-12T15:10:48.488Z" }, - { url = "https://files.pythonhosted.org/packages/61/be/014f7eab51449f3c894aa9bbda2707b5340c85650cb7d0db4ec9ae280501/orjson-3.11.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d08e342a7143f8a7c11f1c4033efe81acbd3c98c68ba1b26b96080396019701f", size = 120700, upload-time = "2025-08-12T15:10:49.811Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ae/c217903a30c51341868e2d8c318c59a8413baa35af54d7845071c8ccd6fe/orjson-3.11.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c0f84fc50398773a702732c87cd622737bf11c0721e6db3041ac7802a686fb", size = 123433, upload-time = "2025-08-12T15:10:51.06Z" }, - { url = "https://files.pythonhosted.org/packages/57/c2/b3c346f78b1ff2da310dd300cb0f5d32167f872b4d3bb1ad122c889d97b0/orjson-3.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:140f84e3c8d4c142575898c91e3981000afebf0333df753a90b3435d349a5fe5", size = 121061, upload-time = "2025-08-12T15:10:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/00/c8/c97798f6010327ffc75ad21dd6bca11ea2067d1910777e798c2849f1c68f/orjson-3.11.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96304a2b7235e0f3f2d9363ddccdbfb027d27338722fe469fe656832a017602e", size = 119410, upload-time = "2025-08-12T15:10:53.692Z" }, - { url = "https://files.pythonhosted.org/packages/37/fd/df720f7c0e35694617b7f95598b11a2cb0374661d8389703bea17217da53/orjson-3.11.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d7612bb227d5d9582f1f50a60bd55c64618fc22c4a32825d233a4f2771a428a", size = 392294, upload-time = "2025-08-12T15:10:55.079Z" }, - { url = "https://files.pythonhosted.org/packages/ba/52/0120d18f60ab0fe47531d520372b528a45c9a25dcab500f450374421881c/orjson-3.11.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a134587d18fe493befc2defffef2a8d27cfcada5696cb7234de54a21903ae89a", size = 134134, upload-time = "2025-08-12T15:10:56.568Z" }, - { url = "https://files.pythonhosted.org/packages/ec/10/1f967671966598366de42f07e92b0fc694ffc66eafa4b74131aeca84915f/orjson-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0b84455e60c4bc12c1e4cbaa5cfc1acdc7775a9da9cec040e17232f4b05458bd", size = 123745, upload-time = "2025-08-12T15:10:57.907Z" }, - { url = "https://files.pythonhosted.org/packages/43/eb/76081238671461cfd0f47e0c24f408ffa66184237d56ef18c33e86abb612/orjson-3.11.2-cp311-cp311-win32.whl", hash = "sha256:f0660efeac223f0731a70884e6914a5f04d613b5ae500744c43f7bf7b78f00f9", size = 124393, upload-time = "2025-08-12T15:10:59.267Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/cc598c1811ba9ba935171267b02e377fc9177489efce525d478a2999d9cc/orjson-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:955811c8405251d9e09cbe8606ad8fdef49a451bcf5520095a5ed38c669223d8", size = 119561, upload-time = "2025-08-12T15:11:00.559Z" }, - { url = "https://files.pythonhosted.org/packages/d8/17/c48011750f0489006f7617b0a3cebc8230f36d11a34e7e9aca2085f07792/orjson-3.11.2-cp311-cp311-win_arm64.whl", hash = "sha256:2e4d423a6f838552e3a6d9ec734b729f61f88b1124fd697eab82805ea1a2a97d", size = 114186, upload-time = "2025-08-12T15:11:01.931Z" }, - { url = "https://files.pythonhosted.org/packages/40/02/46054ebe7996a8adee9640dcad7d39d76c2000dc0377efa38e55dc5cbf78/orjson-3.11.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:901d80d349d8452162b3aa1afb82cec5bee79a10550660bc21311cc61a4c5486", size = 226528, upload-time = "2025-08-12T15:11:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/6b6f0b4d8aea1137436546b990f71be2cd8bd870aa2f5aa14dba0fcc95dc/orjson-3.11.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:cf3bd3967a360e87ee14ed82cb258b7f18c710dacf3822fb0042a14313a673a1", size = 115931, upload-time = "2025-08-12T15:11:04.759Z" }, - { url = "https://files.pythonhosted.org/packages/ae/05/4205cc97c30e82a293dd0d149b1a89b138ebe76afeca66fc129fa2aa4e6a/orjson-3.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26693dde66910078229a943e80eeb99fdce6cd2c26277dc80ead9f3ab97d2131", size = 111382, upload-time = "2025-08-12T15:11:06.468Z" }, - { url = "https://files.pythonhosted.org/packages/50/c7/b8a951a93caa821f9272a7c917115d825ae2e4e8768f5ddf37968ec9de01/orjson-3.11.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad4c8acb50a28211c33fc7ef85ddf5cb18d4636a5205fd3fa2dce0411a0e30c", size = 116271, upload-time = "2025-08-12T15:11:07.845Z" }, - { url = "https://files.pythonhosted.org/packages/17/03/1006c7f8782d5327439e26d9b0ec66500ea7b679d4bbb6b891d2834ab3ee/orjson-3.11.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994181e7f1725bb5f2d481d7d228738e0743b16bf319ca85c29369c65913df14", size = 119086, upload-time = "2025-08-12T15:11:09.329Z" }, - { url = "https://files.pythonhosted.org/packages/44/61/57d22bc31f36a93878a6f772aea76b2184102c6993dea897656a66d18c74/orjson-3.11.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb79a0476393c07656b69c8e763c3cc925fa8e1d9e9b7d1f626901bb5025448", size = 120724, upload-time = "2025-08-12T15:11:10.674Z" }, - { url = "https://files.pythonhosted.org/packages/78/a9/4550e96b4c490c83aea697d5347b8f7eb188152cd7b5a38001055ca5b379/orjson-3.11.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191ed27a1dddb305083d8716af413d7219f40ec1d4c9b0e977453b4db0d6fb6c", size = 123577, upload-time = "2025-08-12T15:11:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/3a/86/09b8cb3ebd513d708ef0c92d36ac3eebda814c65c72137b0a82d6d688fc4/orjson-3.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0afb89f16f07220183fd00f5f297328ed0a68d8722ad1b0c8dcd95b12bc82804", size = 121195, upload-time = "2025-08-12T15:11:13.399Z" }, - { url = "https://files.pythonhosted.org/packages/37/68/7b40b39ac2c1c644d4644e706d0de6c9999764341cd85f2a9393cb387661/orjson-3.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ab6e6b4e93b1573a026b6ec16fca9541354dd58e514b62c558b58554ae04307", size = 119234, upload-time = "2025-08-12T15:11:15.134Z" }, - { url = "https://files.pythonhosted.org/packages/40/7c/bb6e7267cd80c19023d44d8cbc4ea4ed5429fcd4a7eb9950f50305697a28/orjson-3.11.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9cb23527efb61fb75527df55d20ee47989c4ee34e01a9c98ee9ede232abf6219", size = 392250, upload-time = "2025-08-12T15:11:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6730ace05583dbca7c1b406d59f4266e48cd0d360566e71482420fb849fc/orjson-3.11.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a4dd1268e4035af21b8a09e4adf2e61f87ee7bf63b86d7bb0a237ac03fad5b45", size = 134572, upload-time = "2025-08-12T15:11:18.205Z" }, - { url = "https://files.pythonhosted.org/packages/96/0f/7d3e03a30d5aac0432882b539a65b8c02cb6dd4221ddb893babf09c424cc/orjson-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff8b155b145eaf5a9d94d2c476fbe18d6021de93cf36c2ae2c8c5b775763f14e", size = 123869, upload-time = "2025-08-12T15:11:19.554Z" }, - { url = "https://files.pythonhosted.org/packages/45/80/1513265eba6d4a960f078f4b1d2bff94a571ab2d28c6f9835e03dfc65cc6/orjson-3.11.2-cp312-cp312-win32.whl", hash = "sha256:ae3bb10279d57872f9aba68c9931aa71ed3b295fa880f25e68da79e79453f46e", size = 124430, upload-time = "2025-08-12T15:11:20.914Z" }, - { url = "https://files.pythonhosted.org/packages/fb/61/eadf057b68a332351eeb3d89a4cc538d14f31cd8b5ec1b31a280426ccca2/orjson-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:d026e1967239ec11a2559b4146a61d13914504b396f74510a1c4d6b19dfd8732", size = 119598, upload-time = "2025-08-12T15:11:22.372Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3f/7f4b783402143d965ab7e9a2fc116fdb887fe53bdce7d3523271cd106098/orjson-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:59f8d5ad08602711af9589375be98477d70e1d102645430b5a7985fdbf613b36", size = 114052, upload-time = "2025-08-12T15:11:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419, upload-time = "2025-08-12T15:11:25.517Z" }, - { url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803, upload-time = "2025-08-12T15:11:27.357Z" }, - { url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337, upload-time = "2025-08-12T15:11:28.805Z" }, - { url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222, upload-time = "2025-08-12T15:11:30.18Z" }, - { url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020, upload-time = "2025-08-12T15:11:31.59Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721, upload-time = "2025-08-12T15:11:33.035Z" }, - { url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574, upload-time = "2025-08-12T15:11:34.433Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225, upload-time = "2025-08-12T15:11:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201, upload-time = "2025-08-12T15:11:37.642Z" }, - { url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193, upload-time = "2025-08-12T15:11:39.153Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548, upload-time = "2025-08-12T15:11:40.768Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798, upload-time = "2025-08-12T15:11:42.164Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402, upload-time = "2025-08-12T15:11:44.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498, upload-time = "2025-08-12T15:11:45.864Z" }, - { url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051, upload-time = "2025-08-12T15:11:47.555Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406, upload-time = "2025-08-12T15:11:49.445Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788, upload-time = "2025-08-12T15:11:50.992Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318, upload-time = "2025-08-12T15:11:52.495Z" }, - { url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231, upload-time = "2025-08-12T15:11:53.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204, upload-time = "2025-08-12T15:11:55.409Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237, upload-time = "2025-08-12T15:11:57.18Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578, upload-time = "2025-08-12T15:11:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799, upload-time = "2025-08-12T15:12:00.352Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461, upload-time = "2025-08-12T15:12:01.854Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494, upload-time = "2025-08-12T15:12:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046, upload-time = "2025-08-12T15:12:04.87Z" }, + { url = "https://files.pythonhosted.org/packages/9b/64/4a3cef001c6cd9c64256348d4c13a7b09b857e3e1cbb5185917df67d8ced/orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7", size = 238600, upload-time = "2025-08-26T17:44:36.875Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/0c8c87f54f79d051485903dc46226c4d3220b691a151769156054df4562b/orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120", size = 123526, upload-time = "2025-08-26T17:44:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/249497e861f2d438f45b3ab7b7b361484237414945169aa285608f9f7019/orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467", size = 128075, upload-time = "2025-08-26T17:44:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/e5/64/00485702f640a0fd56144042a1ea196469f4a3ae93681871564bf74fa996/orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873", size = 130483, upload-time = "2025-08-26T17:44:41.788Z" }, + { url = "https://files.pythonhosted.org/packages/64/81/110d68dba3909171bf3f05619ad0cf187b430e64045ae4e0aa7ccfe25b15/orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a", size = 132539, upload-time = "2025-08-26T17:44:43.12Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/dba25c22b0ddfafa1e6516a780a00abac28d49f49e7202eb433a53c3e94e/orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b", size = 135390, upload-time = "2025-08-26T17:44:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/1d/ca2230fd55edbd87b58a43a19032d63a4b180389a97520cc62c535b726f9/orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf", size = 132966, upload-time = "2025-08-26T17:44:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b9/96bbc8ed3e47e52b487d504bd6861798977445fbc410da6e87e302dc632d/orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4", size = 131349, upload-time = "2025-08-26T17:44:46.862Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3c/418fbd93d94b0df71cddf96b7fe5894d64a5d890b453ac365120daec30f7/orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc", size = 404087, upload-time = "2025-08-26T17:44:48.079Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a9/2bfd58817d736c2f63608dec0c34857339d423eeed30099b126562822191/orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569", size = 146067, upload-time = "2025-08-26T17:44:49.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/ba/29023771f334096f564e48d82ed855a0ed3320389d6748a9c949e25be734/orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6", size = 135506, upload-time = "2025-08-26T17:44:50.558Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/b5a1eca83f54cb3aa11a9645b8a22f08d97dbd13f27f83aae7c6666a0a05/orjson-3.11.3-cp310-cp310-win32.whl", hash = "sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc", size = 136352, upload-time = "2025-08-26T17:44:51.698Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c0/7ebfaa327d9a9ed982adc0d9420dbce9a3fec45b60ab32c6308f731333fa/orjson-3.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770", size = 131539, upload-time = "2025-08-26T17:44:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/360674cd817faef32e49276187922a946468579fcaf37afdfb6c07046e92/orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f", size = 238238, upload-time = "2025-08-26T17:44:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/05/3d/5fa9ea4b34c1a13be7d9046ba98d06e6feb1d8853718992954ab59d16625/orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91", size = 127713, upload-time = "2025-08-26T17:44:55.596Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/e18367823925e00b1feec867ff5f040055892fc474bf5f7875649ecfa586/orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904", size = 123241, upload-time = "2025-08-26T17:44:57.185Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bd/3c66b91c4564759cf9f473251ac1650e446c7ba92a7c0f9f56ed54f9f0e6/orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6", size = 127895, upload-time = "2025-08-26T17:44:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/82/b5/dc8dcd609db4766e2967a85f63296c59d4722b39503e5b0bf7fd340d387f/orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d", size = 130303, upload-time = "2025-08-26T17:44:59.491Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/d58ec5fd1270b2aa44c862171891adc2e1241bd7dab26c8f46eb97c6c6f1/orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038", size = 132366, upload-time = "2025-08-26T17:45:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/73/87/0ef7e22eb8dd1ef940bfe3b9e441db519e692d62ed1aae365406a16d23d0/orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb", size = 135180, upload-time = "2025-08-26T17:45:02.424Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/e5bf7b70883f374710ad74faf99bacfc4b5b5a7797c1d5e130350e0e28a3/orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2", size = 132741, upload-time = "2025-08-26T17:45:03.663Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0c/4577fd860b6386ffaa56440e792af01c7882b56d2766f55384b5b0e9d39b/orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55", size = 131104, upload-time = "2025-08-26T17:45:04.939Z" }, + { url = "https://files.pythonhosted.org/packages/66/4b/83e92b2d67e86d1c33f2ea9411742a714a26de63641b082bdbf3d8e481af/orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1", size = 403887, upload-time = "2025-08-26T17:45:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e5/9eea6a14e9b5ceb4a271a1fd2e1dec5f2f686755c0fab6673dc6ff3433f4/orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824", size = 145855, upload-time = "2025-08-26T17:45:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/8d4f5ad0c80ba9bf8ac4d0fc71f93a7d0dc0844989e645e2074af376c307/orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f", size = 135361, upload-time = "2025-08-26T17:45:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5f/16386970370178d7a9b438517ea3d704efcf163d286422bae3b37b88dbb5/orjson-3.11.3-cp311-cp311-win32.whl", hash = "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204", size = 136190, upload-time = "2025-08-26T17:45:10.962Z" }, + { url = "https://files.pythonhosted.org/packages/09/60/db16c6f7a41dd8ac9fb651f66701ff2aeb499ad9ebc15853a26c7c152448/orjson-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b", size = 131389, upload-time = "2025-08-26T17:45:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/bb811ad336667041dea9b8565c7c9faf2f59b47eb5ab680315eea612ef2e/orjson-3.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e", size = 126120, upload-time = "2025-08-26T17:45:13.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" }, ] [[package]] @@ -2202,7 +2231,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2213,9 +2242,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] @@ -2233,28 +2262,28 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.2.1" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-mock" -version = "3.14.1" +version = "3.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/99/3323ee5c16b3637b4d941c362182d3e749c11e400bea31018c42219f3a98/pytest_mock-3.15.0.tar.gz", hash = "sha256:ab896bd190316b9d5d87b277569dfcdf718b2d049a2ccff5f7aca279c002a1cf", size = 33838, upload-time = "2025-09-04T20:57:48.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/7fefc43fb706380144bcd293cc6e446e6f637ddfa8b83f48d1734156b529/pytest_mock-3.15.0-py3-none-any.whl", hash = "sha256:ef2219485fb1bd256b00e7ad7466ce26729b30eadfc7cbcdb4fa9a92ca68db6f", size = 10050, upload-time = "2025-09-04T20:57:47.274Z" }, ] [[package]] @@ -2530,27 +2559,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.8" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" }, + { url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" }, + { url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" }, + { url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" }, + { url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" }, ] [[package]] @@ -2873,27 +2903,27 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.4" +version = "0.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/b4/c1ce3699e81977da2ace8b16d2badfd42b060e7d33d75c4ccdbf9dc920fa/tokenizers-0.22.0.tar.gz", hash = "sha256:2e33b98525be8453f355927f3cab312c36cd3e44f4d7e9e97da2fa94d0a49dcb", size = 362771, upload-time = "2025-08-29T10:25:33.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, - { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, - { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, - { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, - { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, - { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b1/18c13648edabbe66baa85fe266a478a7931ddc0cd1ba618802eb7b8d9865/tokenizers-0.22.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:eaa9620122a3fb99b943f864af95ed14c8dfc0f47afa3b404ac8c16b3f2bb484", size = 3081954, upload-time = "2025-08-29T10:25:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/c2/02/c3c454b641bd7c4f79e4464accfae9e7dfc913a777d2e561e168ae060362/tokenizers-0.22.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:71784b9ab5bf0ff3075bceeb198149d2c5e068549c0d18fe32d06ba0deb63f79", size = 2945644, upload-time = "2025-08-29T10:25:23.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/02/d10185ba2fd8c2d111e124c9d92de398aee0264b35ce433f79fb8472f5d0/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec5b71f668a8076802b0241a42387d48289f25435b86b769ae1837cad4172a17", size = 3254764, upload-time = "2025-08-29T10:25:12.445Z" }, + { url = "https://files.pythonhosted.org/packages/13/89/17514bd7ef4bf5bfff58e2b131cec0f8d5cea2b1c8ffe1050a2c8de88dbb/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea8562fa7498850d02a16178105b58803ea825b50dc9094d60549a7ed63654bb", size = 3161654, upload-time = "2025-08-29T10:25:15.493Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d8/bac9f3a7ef6dcceec206e3857c3b61bb16c6b702ed7ae49585f5bd85c0ef/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4136e1558a9ef2e2f1de1555dcd573e1cbc4a320c1a06c4107a3d46dc8ac6e4b", size = 3511484, upload-time = "2025-08-29T10:25:20.477Z" }, + { url = "https://files.pythonhosted.org/packages/aa/27/9c9800eb6763683010a4851db4d1802d8cab9cec114c17056eccb4d4a6e0/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf5954de3962a5fd9781dc12048d24a1a6f1f5df038c6e95db328cd22964206", size = 3712829, upload-time = "2025-08-29T10:25:17.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/e3/b1726dbc1f03f757260fa21752e1921445b5bc350389a8314dd3338836db/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8337ca75d0731fc4860e6204cc24bb36a67d9736142aa06ed320943b50b1e7ed", size = 3408934, upload-time = "2025-08-29T10:25:18.76Z" }, + { url = "https://files.pythonhosted.org/packages/d4/61/aeab3402c26874b74bb67a7f2c4b569dde29b51032c5384db592e7b216f4/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a89264e26f63c449d8cded9061adea7b5de53ba2346fc7e87311f7e4117c1cc8", size = 3345585, upload-time = "2025-08-29T10:25:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d3/498b4a8a8764cce0900af1add0f176ff24f475d4413d55b760b8cdf00893/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:790bad50a1b59d4c21592f9c3cf5e5cf9c3c7ce7e1a23a739f13e01fb1be377a", size = 9322986, upload-time = "2025-08-29T10:25:26.607Z" }, + { url = "https://files.pythonhosted.org/packages/a2/62/92378eb1c2c565837ca3cb5f9569860d132ab9d195d7950c1ea2681dffd0/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:76cf6757c73a10ef10bf06fa937c0ec7393d90432f543f49adc8cab3fb6f26cb", size = 9276630, upload-time = "2025-08-29T10:25:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f0/342d80457aa1cda7654327460f69db0d69405af1e4c453f4dc6ca7c4a76e/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1626cb186e143720c62c6c6b5371e62bbc10af60481388c0da89bc903f37ea0c", size = 9547175, upload-time = "2025-08-29T10:25:29.989Z" }, + { url = "https://files.pythonhosted.org/packages/14/84/8aa9b4adfc4fbd09381e20a5bc6aa27040c9c09caa89988c01544e008d18/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da589a61cbfea18ae267723d6b029b84598dc8ca78db9951d8f5beff72d8507c", size = 9692735, upload-time = "2025-08-29T10:25:32.089Z" }, + { url = "https://files.pythonhosted.org/packages/bf/24/83ee2b1dc76bfe05c3142e7d0ccdfe69f0ad2f1ebf6c726cea7f0874c0d0/tokenizers-0.22.0-cp39-abi3-win32.whl", hash = "sha256:dbf9d6851bddae3e046fedfb166f47743c1c7bd11c640f0691dd35ef0bcad3be", size = 2471915, upload-time = "2025-08-29T10:25:36.411Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9b/0e0bf82214ee20231845b127aa4a8015936ad5a46779f30865d10e404167/tokenizers-0.22.0-cp39-abi3-win_amd64.whl", hash = "sha256:c78174859eeaee96021f248a56c801e36bfb6bd5b067f2e95aa82445ca324f00", size = 2680494, upload-time = "2025-08-29T10:25:35.14Z" }, ] [[package]] @@ -2919,11 +2949,11 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250809" +version = "6.0.12.20250822" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/21/52ffdbddea3c826bc2758d811ccd7f766912de009c5cf096bd5ebba44680/types_pyyaml-6.0.12.20250809.tar.gz", hash = "sha256:af4a1aca028f18e75297da2ee0da465f799627370d74073e96fee876524f61b5", size = 17385, upload-time = "2025-08-09T03:14:34.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/85/90a442e538359ab5c9e30de415006fb22567aa4301c908c09f19e42975c2/types_pyyaml-6.0.12.20250822.tar.gz", hash = "sha256:259f1d93079d335730a9db7cff2bcaf65d7e04b4a56b5927d49a612199b59413", size = 17481, upload-time = "2025-08-22T03:02:16.209Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/3e/0346d09d6e338401ebf406f12eaf9d0b54b315b86f1ec29e34f1a0aedae9/types_pyyaml-6.0.12.20250809-py3-none-any.whl", hash = "sha256:032b6003b798e7de1a1ddfeefee32fac6486bdfe4845e0ae0e7fb3ee4512b52f", size = 20277, upload-time = "2025-08-09T03:14:34.055Z" }, + { url = "https://files.pythonhosted.org/packages/32/8e/8f0aca667c97c0d76024b37cffa39e76e2ce39ca54a38f285a64e6ae33ba/types_pyyaml-6.0.12.20250822-py3-none-any.whl", hash = "sha256:1fe1a5e146aa315483592d292b72a172b65b946a6d98aa6ddd8e4aa838ab7098", size = 20314, upload-time = "2025-08-22T03:02:15.002Z" }, ] [[package]] @@ -2940,29 +2970,29 @@ wheels = [ [[package]] name = "types-setuptools" -version = "80.9.0.20250809" +version = "80.9.0.20250822" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/4f/d78a04083ee3cc0a7c14406afb2f1e7b63e70da95b777571d665d89b1765/types_setuptools-80.9.0.20250809.tar.gz", hash = "sha256:e986ba37ffde364073d76189e1d79d9928fb6f5278c7d07589cde353d0218864", size = 41209, upload-time = "2025-08-09T03:14:24.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/bd/1e5f949b7cb740c9f0feaac430e301b8f1c5f11a81e26324299ea671a237/types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965", size = 41296, upload-time = "2025-08-22T03:02:08.771Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/1d/ad4fd409b377904324cbd2dc3a11e29ba13e2cf603c5a14cd88a35da5be0/types_setuptools-80.9.0.20250809-py3-none-any.whl", hash = "sha256:7c6539b4c7ac7b4ab4db2be66d8a58fb1e28affa3ee3834be48acafd94f5976a", size = 63160, upload-time = "2025-08-09T03:14:23.639Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/475bf15c1cdc172e7a0d665b6e373ebfb1e9bf734d3f2f543d668b07a142/types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3", size = 63179, upload-time = "2025-08-22T03:02:07.643Z" }, ] [[package]] name = "types-simplejson" -version = "3.20.0.20250326" +version = "3.20.0.20250822" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489, upload-time = "2025-03-26T02:53:35.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462, upload-time = "2025-03-26T02:53:35.036Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, ] [[package]] name = "types-ujson" -version = "5.10.0.20250326" +version = "5.10.0.20250822" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340, upload-time = "2025-03-26T02:53:39.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/bd/d372d44534f84864a96c19a7059d9b4d29db8541828b8b9dc3040f7a46d0/types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006", size = 8437, upload-time = "2025-08-22T03:02:19.433Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644, upload-time = "2025-03-26T02:53:38.2Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f2/d812543c350674d8b3f6e17c8922248ee3bb752c2a76f64beb8c538b40cf/types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080", size = 7657, upload-time = "2025-08-22T03:02:18.699Z" }, ] [[package]] diff --git a/misc/release/archive-version.js b/misc/release/archive-version.js index 3ef4f58b1e..1a66963dad 100755 --- a/misc/release/archive-version.js +++ b/misc/release/archive-version.js @@ -10,7 +10,7 @@ if (!nextVersion) { const filename = './docs/static/archived-versions.json'; const oldVersions = JSON.parse(readFileSync(filename)); const newVersions = [ - { label: `v${nextVersion}`, url: `https://v${nextVersion}.archive.immich.app` }, + { label: `v${nextVersion}`, url: `https://docs.v${nextVersion}.archive.immich.app` }, ...oldVersions, ]; diff --git a/misc/release/pump-version.sh b/misc/release/pump-version.sh index 789805255b..65a2e70e50 100755 --- a/misc/release/pump-version.sh +++ b/misc/release/pump-version.sh @@ -65,7 +65,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then pnpm install --frozen-lockfile --prefix server pnpm --prefix server run build - make open-api + ( cd ./open-api && bash ./bin/generate-open-api.sh ) jq --arg version "$NEXT_SERVER" '.version = $version' open-api/typescript-sdk/package.json > open-api/typescript-sdk/package.json.tmp && mv open-api/typescript-sdk/package.json.tmp open-api/typescript-sdk/package.json @@ -80,7 +80,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then jq --arg version "$NEXT_SERVER" '.version = $version' e2e/package.json > e2e/package.json.tmp && mv e2e/package.json.tmp e2e/package.json pnpm install --frozen-lockfile --prefix e2e - uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "$SERVER_PUMP" + uvx --from=toml-cli toml set --toml-path=machine-learning/pyproject.toml project.version "$NEXT_SERVER" fi if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..2008fa5e4a --- /dev/null +++ b/mise.toml @@ -0,0 +1,512 @@ +[tools] +node = "22.20.0" +flutter = "3.35.4" +pnpm = "10.15.1" + +[tools."github:CQLabs/homebrew-dcm"] +version = "1.30.0" +bin = "dcm" +postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm" + +[settings] +experimental = true +pin = true + +# .github +[tasks."github:install"] +run = "pnpm install --filter github --frozen-lockfile" + +[tasks."github:format"] +env._.path = "./.github/node_modules/.bin" +dir = ".github" +run = "prettier --check ." + +[tasks."github:format-fix"] +env._.path = "./.github/node_modules/.bin" +dir = ".github" +run = "prettier --write ." + +# @immich/cli +[tasks."cli:install"] +run = "pnpm install --filter @immich/cli --frozen-lockfile" + +[tasks."cli:build"] +env._.path = "./cli/node_modules/.bin" +dir = "cli" +run = "vite build" + +[tasks."cli:test"] +env._.path = "./cli/node_modules/.bin" +dir = "cli" +run = "vite" + +[tasks."cli:lint"] +env._.path = "./cli/node_modules/.bin" +dir = "cli" +run = "eslint \"src/**/*.ts\" --max-warnings 0" + +[tasks."cli:lint-fix"] +run = "mise run cli:lint --fix" + +[tasks."cli:format"] +env._.path = "./cli/node_modules/.bin" +dir = "cli" +run = "prettier --check ." + +[tasks."cli:format-fix"] +env._.path = "./cli/node_modules/.bin" +dir = "cli" +run = "prettier --write ." + +[tasks."cli:check"] +env._.path = "./cli/node_modules/.bin" +dir = "cli" +run = "tsc --noEmit" + +# @immich/sdk +[tasks."sdk:install"] +run = "pnpm install --filter @immich/sdk --frozen-lockfile" + +[tasks."sdk:build"] +env._.path = "./open-api/typescript-sdk/node_modules/.bin" +dir = "./open-api/typescript-sdk" +run = "tsc" + +# docs +[tasks."docs:install"] +run = "pnpm install --filter documentation --frozen-lockfile" + +[tasks."docs:start"] +env._.path = "./docs/node_modules/.bin" +dir = "docs" +run = "docusaurus --port 3005" + +[tasks."docs:build"] +env._.path = "./docs/node_modules/.bin" +dir = "docs" +run = [ + "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0", + "docusaurus build", +] + + +[tasks."docs:preview"] +env._.path = "./docs/node_modules/.bin" +dir = "docs" +run = "docusaurus serve" + + +[tasks."docs:format"] +env._.path = "./docs/node_modules/.bin" +dir = "docs" +run = "prettier --check ." + +[tasks."docs:format-fix"] +env._.path = "./docs/node_modules/.bin" +dir = "docs" +run = "prettier --write ." + + +# e2e +[tasks."e2e:install"] +run = "pnpm install --filter immich-e2e --frozen-lockfile" + +[tasks."e2e:test"] +env._.path = "./e2e/node_modules/.bin" +dir = "e2e" +run = "vitest --run" + +[tasks."e2e:test-web"] +env._.path = "./e2e/node_modules/.bin" +dir = "e2e" +run = "playwright test" + +[tasks."e2e:format"] +env._.path = "./e2e/node_modules/.bin" +dir = "e2e" +run = "prettier --check ." + +[tasks."e2e:format-fix"] +env._.path = "./e2e/node_modules/.bin" +dir = "e2e" +run = "prettier --write ." + +[tasks."e2e:lint"] +env._.path = "./e2e/node_modules/.bin" +dir = "e2e" +run = "eslint \"src/**/*.ts\" --max-warnings 0" + +[tasks."e2e:lint-fix"] +run = "mise run e2e:lint --fix" + +[tasks."e2e:check"] +env._.path = "./e2e/node_modules/.bin" +dir = "e2e" +run = "tsc --noEmit" + +# i18n +[tasks."i18n:format"] +run = "mise run i18n:format-fix" + +[tasks."i18n:format-fix"] +run = "pnpm dlx sort-json ./i18n/*.json" + + +# server +[tasks."server:install"] +run = "pnpm install --filter immich --frozen-lockfile" + +[tasks."server:build"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "nest build" + +[tasks."server:test"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "vitest --config test/vitest.config.mjs" + +[tasks."server:test-medium"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "vitest --config test/vitest.config.medium.mjs" + +[tasks."server:format"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "prettier --check ." + +[tasks."server:format-fix"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "prettier --write ." + +[tasks."server:lint"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0" + +[tasks."server:lint-fix"] +run = "mise run server:lint --fix" + +[tasks."server:check"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "tsc --noEmit" + +[tasks."server:sql"] +dir = "server" +run = "node ./dist/bin/sync-open-api.js" + +[tasks."server:open-api"] +dir = "server" +run = "node ./dist/bin/sync-open-api.js" + +[tasks."server:migrations"] +dir = "server" +run = "node ./dist/bin/migrations.js" +description = "Run database migration commands (create, generate, run, debug, or query)" + +[tasks."server:schema-drop"] +run = "mise run server:migrations query 'DROP schema public cascade; CREATE schema public;'" + +[tasks."server:schema-reset"] +run = "mise run server:schema-drop && mise run server:migrations run" + +[tasks."server:email-dev"] +env._.path = "./server/node_modules/.bin" +dir = "server" +run = "email dev -p 3050 --dir src/emails" + +[tasks."server:checklist"] +run = [ + "mise run server:install", + "mise run server:format", + "mise run server:lint", + "mise run server:check", + "mise run server:test-medium --run", + "mise run server:test --run", +] + + +# web +[tasks."web:install"] +run = "pnpm install --filter immich-web --frozen-lockfile" + +[tasks."web:svelte-kit-sync"] +env._.path = "./web/node_modules/.bin" +dir = "web" +run = "svelte-kit sync" + +[tasks."web:build"] +env._.path = "./web/node_modules/.bin" +dir = "web" +run = "vite build" + +[tasks."web:build-stats"] +env.BUILD_STATS = "true" +env._.path = "./web/node_modules/.bin" +dir = "web" +run = "vite build" + +[tasks."web:preview"] +env._.path = "./web/node_modules/.bin" +dir = "web" +run = "vite preview" + +[tasks."web:start"] +env._.path = "web/node_modules/.bin" +dir = "web" +run = "vite dev --host 0.0.0.0 --port 3000" + +[tasks."web:test"] +depends = "web:svelte-kit-sync" +env._.path = "web/node_modules/.bin" +dir = "web" +run = "vitest" + +[tasks."web:format"] +env._.path = "web/node_modules/.bin" +dir = "web" +run = "prettier --check ." + +[tasks."web:format-fix"] +env._.path = "web/node_modules/.bin" +dir = "web" +run = "prettier --write ." + +[tasks."web:lint"] +env._.path = "web/node_modules/.bin" +dir = "web" +run = "eslint . --max-warnings 0" + +[tasks."web:lint-p"] +env._.path = "web/node_modules/.bin" +dir = "web" +run = "eslint-p . --max-warnings 0 --concurrency=4" + +[tasks."web:lint-fix"] +run = "mise run web:lint --fix" + +[tasks."web:check"] +depends = "web:svelte-kit-sync" +env._.path = "web/node_modules/.bin" +dir = "web" +run = "tsc --noEmit" + +[tasks."web:check-svelte"] +depends = "web:svelte-kit-sync" +env._.path = "web/node_modules/.bin" +dir = "web" +run = "svelte-check --no-tsconfig --fail-on-warnings" + +[tasks."web:checklist"] +run = [ + "mise run web:install", + "mise run web:format", + "mise run web:check", + "mise run web:test --run", + "mise run web:lint", +] + + +# mobile +[tasks."mobile:codegen:dart"] +alias = "mobile:codegen" +description = "Execute build_runner to auto-generate dart code" +dir = "mobile" +sources = [ + "pubspec.yaml", + "build.yaml", + "lib/**/*.dart", + "infrastructure/**/*.drift", +] +outputs = { auto = true } +run = "dart run build_runner build --delete-conflicting-outputs" + +[tasks."mobile:codegen:pigeon"] +alias = "mobile:pigeon" +description = "Generate pigeon platform code" +dir = "mobile" +depends = [ + "mobile:pigeon:native-sync", + "mobile:pigeon:thumbnail", + "mobile:pigeon:background-worker", + "mobile:pigeon:background-worker-lock", + "mobile:pigeon:connectivity", +] + +[tasks."mobile:codegen:translation"] +alias = "mobile:translation" +description = "Generate translations from i18n JSONs" +dir = "mobile" +run = [ + { task = "i18n:format-fix" }, + { tasks = [ + "mobile:i18n:loader", + "mobile:i18n:keys", + ] }, +] + +[tasks."mobile:codegen:app-icon"] +description = "Generate app icons" +dir = "mobile" +run = "flutter pub run flutter_launcher_icons:main" + +[tasks."mobile:codegen:splash"] +description = "Generate splash screen" +dir = "mobile" +run = "flutter pub run flutter_native_splash:create" + +[tasks."mobile:test"] +description = "Run mobile tests" +dir = "mobile" +run = "flutter test" + +[tasks."mobile:lint"] +description = "Analyze Dart code" +dir = "mobile" +depends = ["mobile:analyze:dart", "mobile:analyze:dcm"] + +[tasks."mobile:lint-fix"] +description = "Auto-fix Dart code" +dir = "mobile" +depends = ["mobile:analyze:fix:dart", "mobile:analyze:fix:dcm"] + +[tasks."mobile:format"] +description = "Format Dart code" +dir = "mobile" +run = "dart format --set-exit-if-changed $(find lib -name '*.dart' -not \\( -name '*.g.dart' -o -name '*.drift.dart' -o -name '*.gr.dart' \\))" + +[tasks."mobile:build:android"] +description = "Build Android release" +dir = "mobile" +run = "flutter build appbundle" + +[tasks."mobile:drift:migration"] +alias = "mobile:migration" +description = "Generate database migrations" +dir = "mobile" +run = "dart run drift_dev make-migrations" + + +# mobile internal tasks +[tasks."mobile:pigeon:native-sync"] +description = "Generate native sync API pigeon code" +dir = "mobile" +hide = true +sources = ["pigeon/native_sync_api.dart"] +outputs = [ + "lib/platform/native_sync_api.g.dart", + "ios/Runner/Sync/Messages.g.swift", + "android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt", +] +run = [ + "dart run pigeon --input pigeon/native_sync_api.dart", + "dart format lib/platform/native_sync_api.g.dart", +] + +[tasks."mobile:pigeon:thumbnail"] +description = "Generate thumbnail API pigeon code" +dir = "mobile" +hide = true +sources = ["pigeon/thumbnail_api.dart"] +outputs = [ + "lib/platform/thumbnail_api.g.dart", + "ios/Runner/Images/Thumbnails.g.swift", + "android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt", +] +run = [ + "dart run pigeon --input pigeon/thumbnail_api.dart", + "dart format lib/platform/thumbnail_api.g.dart", +] + +[tasks."mobile:pigeon:background-worker"] +description = "Generate background worker API pigeon code" +dir = "mobile" +hide = true +sources = ["pigeon/background_worker_api.dart"] +outputs = [ + "lib/platform/background_worker_api.g.dart", + "ios/Runner/Background/BackgroundWorker.g.swift", + "android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt", +] +run = [ + "dart run pigeon --input pigeon/background_worker_api.dart", + "dart format lib/platform/background_worker_api.g.dart", +] + +[tasks."mobile:pigeon:background-worker-lock"] +description = "Generate background worker lock API pigeon code" +dir = "mobile" +hide = true +sources = ["pigeon/background_worker_lock_api.dart"] +outputs = [ + "lib/platform/background_worker_lock_api.g.dart", + "android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt", +] +run = [ + "dart run pigeon --input pigeon/background_worker_lock_api.dart", + "dart format lib/platform/background_worker_lock_api.g.dart", +] + +[tasks."mobile:pigeon:connectivity"] +description = "Generate connectivity API pigeon code" +dir = "mobile" +hide = true +sources = ["pigeon/connectivity_api.dart"] +outputs = [ + "lib/platform/connectivity_api.g.dart", + "ios/Runner/Connectivity/Connectivity.g.swift", + "android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt", +] +run = [ + "dart run pigeon --input pigeon/connectivity_api.dart", + "dart format lib/platform/connectivity_api.g.dart", +] + +[tasks."mobile:i18n:loader"] +description = "Generate i18n loader" +dir = "mobile" +hide = true +sources = ["i18n/"] +outputs = "lib/generated/codegen_loader.g.dart" +run = [ + "dart run easy_localization:generate -S ../i18n", + "dart format lib/generated/codegen_loader.g.dart", +] + +[tasks."mobile:i18n:keys"] +description = "Generate i18n keys" +dir = "mobile" +hide = true +sources = ["i18n/en.json"] +outputs = "lib/generated/intl_keys.g.dart" +run = [ + "dart run bin/generate_keys.dart", + "dart format lib/generated/intl_keys.g.dart", +] + +[tasks."mobile:analyze:dart"] +description = "Run Dart analysis" +dir = "mobile" +hide = true +run = "dart analyze --fatal-infos" + +[tasks."mobile:analyze:dcm"] +description = "Run Dart Code Metrics" +dir = "mobile" +hide = true +run = "dcm analyze lib --fatal-style --fatal-warnings" + +[tasks."mobile:analyze:fix:dart"] +description = "Auto-fix Dart analysis" +dir = "mobile" +hide = true +run = "dart fix --apply" + +[tasks."mobile:analyze:fix:dcm"] +description = "Auto-fix Dart Code Metrics" +dir = "mobile" +hide = true +run = "dcm fix lib" diff --git a/mobile/.fvmrc b/mobile/.fvmrc index 3ca65ffc7c..a4d5f6d9b7 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.32.8" + "flutter": "3.35.4" } \ No newline at end of file diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 9a9fb67ce3..9c6057e582 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,8 +1,8 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.32.8", + "dart.flutterSdkPath": ".fvm/versions/3.35.4", "dart.lineLength": 120, "[dart]": { - "editor.rulers": [120], + "editor.rulers": [120] }, "search.exclude": { "**/.fvm": true diff --git a/mobile/README.md b/mobile/README.md index 436b0a4c34..59b2d9340c 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -84,4 +84,4 @@ Below is how your code needs to be structured: ## Contributing -Please refer to the [architecture](https://immich.app/docs/developer/architecture/) for contributing to the mobile app! +Please refer to the [architecture](https://docs.immich.app/developer/architecture/) for contributing to the mobile app! diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 1b0b7170d2..c04e1dafdc 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -43,8 +43,9 @@ analyzer: - lib/**/*.g.dart - lib/**/*.drift.dart - plugins: - - custom_lint + # TODO: Re-enable after upgrading custom_lint + # plugins: + # - custom_lint custom_lint: debug: true @@ -81,6 +82,7 @@ custom_lint: # acceptable exceptions for the time being (until Isar is fully replaced) - lib/providers/app_life_cycle.provider.dart - integration_test/test_utils/general_helper.dart + - lib/domain/services/background_worker.service.dart - lib/main.dart - lib/pages/album/album_asset_selection.page.dart - lib/routing/router.dart @@ -133,6 +135,13 @@ custom_lint: dart_code_metrics: rules: + - banned-usage: + entries: + - name: debugPrint + description: Use dPrint instead of debugPrint for proper tree-shaking in release builds. + exclude-paths: + - 'lib/utils/debug_print.dart' + severity: perf # All rules from "recommended" preset # Show potential errors # - avoid-cascade-after-if-null diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index ff806870f9..5a3b0e1f3d 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -3,17 +3,20 @@ package app.alextran.immich import android.app.Application import androidx.work.Configuration import androidx.work.WorkManager +import app.alextran.immich.background.BackgroundWorkerApiImpl class ImmichApp : Application() { - override fun onCreate() { - super.onCreate() - val config = Configuration.Builder().build() - WorkManager.initialize(this, config) - // always start BackupWorker after WorkManager init; this fixes the following bug: - // After the process is killed (by user or system), the first trigger (taking a new picture) is lost. - // Thus, the BackupWorker is not started. If the system kills the process after each initialization - // (because of low memory etc.), the backup is never performed. - // As a workaround, we also run a backup check when initializing the application - ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) - } -} \ No newline at end of file + override fun onCreate() { + super.onCreate() + val config = Configuration.Builder().build() + WorkManager.initialize(this, config) + // always start BackupWorker after WorkManager init; this fixes the following bug: + // After the process is killed (by user or system), the first trigger (taking a new picture) is lost. + // Thus, the BackupWorker is not started. If the system kills the process after each initialization + // (because of low memory etc.), the backup is never performed. + // As a workaround, we also run a backup check when initializing the application + + ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) + BackgroundWorkerApiImpl.enqueueBackgroundWorker(this) + } +} 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 b1a50695a3..034f5ee72e 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,8 +1,14 @@ package app.alextran.immich +import android.content.Context import android.os.Build import android.os.ext.SdkExtensions -import androidx.annotation.NonNull +import app.alextran.immich.background.BackgroundEngineLock +import app.alextran.immich.background.BackgroundWorkerApiImpl +import app.alextran.immich.background.BackgroundWorkerFgHostApi +import app.alextran.immich.background.BackgroundWorkerLockApi +import app.alextran.immich.connectivity.ConnectivityApi +import app.alextran.immich.connectivity.ConnectivityApiImpl import app.alextran.immich.images.ThumbnailApi import app.alextran.immich.images.ThumbnailsImpl import app.alextran.immich.sync.NativeSyncApi @@ -12,19 +18,30 @@ import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine class MainActivity : FlutterFragmentActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + override fun configureFlutterEngine(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 + registerPlugins(this, flutterEngine) + } - 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) - ThumbnailApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ThumbnailsImpl(this)) + companion object { + fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) { + val messenger = flutterEngine.dartExecutor.binaryMessenger + val backgroundEngineLockImpl = BackgroundEngineLock(ctx) + BackgroundWorkerLockApi.setUp(messenger, backgroundEngineLockImpl) + val nativeSyncApiImpl = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) { + NativeSyncApiImpl26(ctx) + } else { + NativeSyncApiImpl30(ctx) + } + NativeSyncApi.setUp(messenger, nativeSyncApiImpl) + ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx)) + BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) + ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) + + flutterEngine.plugins.add(BackgroundServicePlugin()) + flutterEngine.plugins.add(HttpSSLOptionsPlugin()) + flutterEngine.plugins.add(backgroundEngineLockImpl) + } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt new file mode 100644 index 0000000000..d8afe32b5c --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt @@ -0,0 +1,50 @@ +package app.alextran.immich.background + +import android.content.Context +import android.util.Log +import io.flutter.embedding.engine.plugins.FlutterPlugin +import java.util.concurrent.atomic.AtomicInteger + +private const val TAG = "BackgroundEngineLock" + +class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, FlutterPlugin { + private val ctx: Context = context.applicationContext + + companion object { + + private var engineCount = AtomicInteger(0) + + private fun checkAndEnforceBackgroundLock(ctx: Context) { + // work manager task is running while the main app is opened, cancel the worker + if (BackgroundWorkerPreferences(ctx).isLocked() && + engineCount.get() > 1 && + BackgroundWorkerApiImpl.isBackgroundWorkerRunning() + ) { + Log.i(TAG, "Background worker is locked, cancelling the background worker") + BackgroundWorkerApiImpl.cancelBackgroundWorker(ctx) + } + } + } + + override fun lock() { + BackgroundWorkerPreferences(ctx).setLocked(true) + checkAndEnforceBackgroundLock(ctx) + Log.i(TAG, "Background worker is locked") + } + + override fun unlock() { + BackgroundWorkerPreferences(ctx).setLocked(false) + Log.i(TAG, "Background worker is unlocked") + } + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + checkAndEnforceBackgroundLock(binding.applicationContext) + engineCount.incrementAndGet() + Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount") + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + engineCount.decrementAndGet() + Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount") + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt new file mode 100644 index 0000000000..052395c172 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt @@ -0,0 +1,332 @@ +// Autogenerated from Pigeon (v26.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.background + +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 BackgroundWorkerPigeonUtils { + + fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } + + 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 BackgroundWorkerSettings ( + val requiresCharging: Boolean, + val minimumDelaySeconds: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): BackgroundWorkerSettings { + val requiresCharging = pigeonVar_list[0] as Boolean + val minimumDelaySeconds = pigeonVar_list[1] as Long + return BackgroundWorkerSettings(requiresCharging, minimumDelaySeconds) + } + } + fun toList(): List { + return listOf( + requiresCharging, + minimumDelaySeconds, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is BackgroundWorkerSettings) { + return false + } + if (this === other) { + return true + } + return BackgroundWorkerPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + BackgroundWorkerSettings.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is BackgroundWorkerSettings -> { + stream.write(129) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface BackgroundWorkerFgHostApi { + fun enable() + fun configure(settings: BackgroundWorkerSettings) + fun disable() + + companion object { + /** The codec used by BackgroundWorkerFgHostApi. */ + val codec: MessageCodec by lazy { + BackgroundWorkerPigeonCodec() + } + /** Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.enable() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val settingsArg = args[0] as BackgroundWorkerSettings + val wrapped: List = try { + api.configure(settingsArg) + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.disable() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface BackgroundWorkerBgHostApi { + fun onInitialized() + fun showNotification(title: String, content: String) + fun close() + + companion object { + /** The codec used by BackgroundWorkerBgHostApi. */ + val codec: MessageCodec by lazy { + BackgroundWorkerPigeonCodec() + } + /** Sets up an instance of `BackgroundWorkerBgHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerBgHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.onInitialized() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val titleArg = args[0] as String + val contentArg = args[1] as String + val wrapped: List = try { + api.showNotification(titleArg, contentArg) + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.close() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by BackgroundWorkerFlutterApi. */ + val codec: MessageCodec by lazy { + BackgroundWorkerPigeonCodec() + } + } + fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(isRefreshArg, maxSecondsArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onAndroidUpload(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) + } + } + } + fun cancel(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt new file mode 100644 index 0000000000..71d9f5ffe3 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt @@ -0,0 +1,223 @@ +package app.alextran.immich.background + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.work.ForegroundInfo +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters +import app.alextran.immich.MainActivity +import app.alextran.immich.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture +import io.flutter.FlutterInjector +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.embedding.engine.loader.FlutterLoader +import java.util.concurrent.TimeUnit + +private const val TAG = "BackgroundWorker" + +class BackgroundWorker(context: Context, params: WorkerParameters) : + ListenableWorker(context, params), BackgroundWorkerBgHostApi { + private val ctx: Context = context.applicationContext + + /// The Flutter loader that loads the native Flutter library and resources. + /// This must be initialized before starting the Flutter engine. + private var loader: FlutterLoader = FlutterInjector.instance().flutterLoader() + + /// The Flutter engine created specifically for background execution. + /// This is a separate instance from the main Flutter engine that handles the UI. + /// It operates in its own isolate and doesn't share memory with the main engine. + /// Must be properly started, registered, and torn down during background execution. + private var engine: FlutterEngine? = null + + // Used to call methods on the flutter side + private var flutterApi: BackgroundWorkerFlutterApi? = null + + /// Result returned when the background task completes. This is used to signal + /// to the WorkManager that the task has finished, either successfully or with failure. + private val completionHandler: SettableFuture = SettableFuture.create() + + /// Flag to track whether the background task has completed to prevent duplicate completions + private var isComplete = false + + private val notificationManager = + ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private var foregroundFuture: ListenableFuture? = null + + companion object { + private const val NOTIFICATION_CHANNEL_ID = "immich::background_worker::notif" + private const val NOTIFICATION_ID = 100 + } + + override fun startWork(): ListenableFuture { + Log.i(TAG, "Starting background upload worker") + + if (!loader.initialized()) { + loader.startInitialization(ctx) + } + + val notificationChannel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(notificationChannel) + + loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { + engine = FlutterEngine(ctx) + FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!) + + // Register custom plugins + MainActivity.registerPlugins(ctx, engine!!) + flutterApi = + BackgroundWorkerFlutterApi(binaryMessenger = engine!!.dartExecutor.binaryMessenger) + BackgroundWorkerBgHostApi.setUp( + binaryMessenger = engine!!.dartExecutor.binaryMessenger, + api = this + ) + + engine!!.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint( + loader.findAppBundlePath(), + "package:immich_mobile/domain/services/background_worker.service.dart", + "backgroundSyncNativeEntrypoint" + ) + ) + } + + return completionHandler + } + + /** + * Called by the Flutter side when it has finished initialization and is ready to receive commands. + * Routes the appropriate task type (refresh or processing) to the corresponding Flutter method. + * This method acts as a bridge between the native Android background task system and Flutter. + */ + override fun onInitialized() { + flutterApi?.onAndroidUpload { handleHostResult(it) } + } + + // TODO: Move this to a separate NotificationManager class + override fun showNotification(title: String, content: String) { + val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.notification_icon) + .setOnlyAlertOnce(true) + .setOngoing(true) + .setTicker(title) + .setContentTitle(title) + .setContentText(content) + .build() + + if (isIgnoringBatteryOptimizations()) { + foregroundFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setForegroundAsync( + ForegroundInfo( + NOTIFICATION_ID, + notification, + FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + ) + } else { + setForegroundAsync(ForegroundInfo(NOTIFICATION_ID, notification)) + } + } else { + notificationManager.notify(NOTIFICATION_ID, notification) + } + } + + override fun close() { + if (isComplete) { + return + } + + Handler(Looper.getMainLooper()).postAtFrontOfQueue { + if (flutterApi != null) { + flutterApi?.cancel { + complete(Result.failure()) + } + } + } + + waitForForegroundPromotion() + + Handler(Looper.getMainLooper()).postDelayed({ + complete(Result.failure()) + }, 5000) + } + + /** + * Called when the system has to stop this worker because constraints are + * no longer met or the system needs resources for more important tasks + * This is also called when the worker has been explicitly cancelled or replaced + */ + override fun onStopped() { + Log.d(TAG, "About to stop BackupWorker") + close() + } + + private fun handleHostResult(result: kotlin.Result) { + if (isComplete) { + return + } + + result.fold( + onSuccess = { _ -> complete(Result.success()) }, + onFailure = { _ -> onStopped() } + ) + } + + /** + * Cleans up resources by destroying the Flutter engine context and invokes the completion handler. + * This method ensures that the background task is marked as complete, releases the Flutter engine, + * and notifies the caller of the task's success or failure. This is the final step in the + * background task lifecycle and should only be called once per task instance. + * + * - Parameter success: Indicates whether the background task completed successfully + */ + private fun complete(success: Result) { + Log.d(TAG, "About to complete BackupWorker with result: $success") + isComplete = true + engine?.destroy() + engine = null + flutterApi = null + notificationManager.cancel(NOTIFICATION_ID) + FlutterEngineCache.getInstance().remove(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY) + waitForForegroundPromotion() + completionHandler.set(success) + } + + /** + * Returns `true` if the app is ignoring battery optimizations + */ + private fun isIgnoringBatteryOptimizations(): Boolean { + val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) + } + + /** + * Calls to setForegroundAsync() that do not complete before completion of a ListenableWorker will signal an IllegalStateException + * https://android-review.googlesource.com/c/platform/frameworks/support/+/1262743 + * Wait for a short period of time for the foreground promotion to complete before completing the worker + */ + private fun waitForForegroundPromotion() { + val foregroundFuture = this.foregroundFuture + if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) { + try { + foregroundFuture.get(500, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + // ignored, there is nothing to be done + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt new file mode 100644 index 0000000000..78f2e9e461 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt @@ -0,0 +1,92 @@ +package app.alextran.immich.background + +import android.content.Context +import android.provider.MediaStore +import android.util.Log +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import io.flutter.embedding.engine.FlutterEngineCache +import java.util.concurrent.TimeUnit + +private const val TAG = "BackgroundWorkerApiImpl" + +class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { + private val ctx: Context = context.applicationContext + + override fun enable() { + enqueueMediaObserver(ctx) + } + + override fun configure(settings: BackgroundWorkerSettings) { + BackgroundWorkerPreferences(ctx).updateSettings(settings) + enqueueMediaObserver(ctx) + } + + override fun disable() { + WorkManager.getInstance(ctx).apply { + cancelUniqueWork(OBSERVER_WORKER_NAME) + cancelUniqueWork(BACKGROUND_WORKER_NAME) + } + Log.i(TAG, "Cancelled background upload tasks") + } + + companion object { + private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1" + private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1" + const val ENGINE_CACHE_KEY = "immich::background_worker::engine" + + + fun enqueueMediaObserver(ctx: Context) { + val settings = BackgroundWorkerPreferences(ctx).getSettings() + val constraints = Constraints.Builder().apply { + addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) + addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) + addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) + addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) + setTriggerContentUpdateDelay(settings.minimumDelaySeconds, TimeUnit.SECONDS) + setTriggerContentMaxDelay(settings.minimumDelaySeconds * 10, TimeUnit.SECONDS) + setRequiresCharging(settings.requiresCharging) + }.build() + + val work = OneTimeWorkRequest.Builder(MediaObserver::class.java) + .setConstraints(constraints) + .build() + WorkManager.getInstance(ctx) + .enqueueUniqueWork(OBSERVER_WORKER_NAME, ExistingWorkPolicy.REPLACE, work) + + Log.i( + TAG, + "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME and settings: $settings" + ) + } + + fun enqueueBackgroundWorker(ctx: Context) { + val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build() + + val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(ctx) + .enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.KEEP, work) + + Log.i(TAG, "Enqueued background worker with name: $BACKGROUND_WORKER_NAME") + } + + fun isBackgroundWorkerRunning(): Boolean { + // Easier to check if the engine is cached as we always cache the engine when starting the worker + // and remove it when the worker is finished + return FlutterEngineCache.getInstance().get(ENGINE_CACHE_KEY) != null + } + + fun cancelBackgroundWorker(ctx: Context) { + WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME) + FlutterEngineCache.getInstance().remove(ENGINE_CACHE_KEY) + + Log.i(TAG, "Cancelled background upload task") + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt new file mode 100644 index 0000000000..3d00bafba2 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt @@ -0,0 +1,95 @@ +// Autogenerated from Pigeon (v26.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.background + +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 BackgroundWorkerLockPigeonUtils { + + 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) + ) + } + } +} +private open class BackgroundWorkerLockPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return super.readValueOfType(type, buffer) + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + super.writeValue(stream, value) + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface BackgroundWorkerLockApi { + fun lock() + fun unlock() + + companion object { + /** The codec used by BackgroundWorkerLockApi. */ + val codec: MessageCodec by lazy { + BackgroundWorkerLockPigeonCodec() + } + /** Sets up an instance of `BackgroundWorkerLockApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerLockApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.lock() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerLockPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.unlock() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerLockPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt new file mode 100644 index 0000000000..cfceb06c1d --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt @@ -0,0 +1,51 @@ +package app.alextran.immich.background + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit + +class BackgroundWorkerPreferences(private val ctx: Context) { + companion object { + const val SHARED_PREF_NAME = "Immich::BackgroundWorker" + private const val SHARED_PREF_MIN_DELAY_KEY = "BackgroundWorker::minDelaySeconds" + private const val SHARED_PREF_REQUIRE_CHARGING_KEY = "BackgroundWorker::requireCharging" + private const val SHARED_PREF_LOCK_KEY = "BackgroundWorker::isLocked" + + private const val DEFAULT_MIN_DELAY_SECONDS = 30L + private const val DEFAULT_REQUIRE_CHARGING = false + } + + private val sp: SharedPreferences by lazy { + ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + } + + fun updateSettings(settings: BackgroundWorkerSettings) { + sp.edit { + putLong(SHARED_PREF_MIN_DELAY_KEY, settings.minimumDelaySeconds) + putBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, settings.requiresCharging) + } + } + + fun getSettings(): BackgroundWorkerSettings { + val delaySeconds = sp.getLong(SHARED_PREF_MIN_DELAY_KEY, DEFAULT_MIN_DELAY_SECONDS) + + return BackgroundWorkerSettings( + minimumDelaySeconds = if (delaySeconds >= 1000) delaySeconds / 1000 else delaySeconds, + requiresCharging = sp.getBoolean( + SHARED_PREF_REQUIRE_CHARGING_KEY, + DEFAULT_REQUIRE_CHARGING + ), + ) + } + + fun setLocked(paused: Boolean) { + sp.edit { + putBoolean(SHARED_PREF_LOCK_KEY, paused) + } + } + + fun isLocked(): Boolean { + return sp.getBoolean(SHARED_PREF_LOCK_KEY, true) + } +} + diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt new file mode 100644 index 0000000000..7283411ac0 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt @@ -0,0 +1,22 @@ +package app.alextran.immich.background + +import android.content.Context +import android.util.Log +import androidx.work.Worker +import androidx.work.WorkerParameters + +class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) { + private val ctx: Context = context.applicationContext + + override fun doWork(): Result { + Log.i("MediaObserver", "Content change detected, starting background worker") + // Re-enqueue itself to listen for future changes + BackgroundWorkerApiImpl.enqueueMediaObserver(ctx) + + // Enqueue backup worker only if there are new media changes + if (triggeredContentUris.isNotEmpty()) { + BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx) + } + return Result.success() + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt new file mode 100644 index 0000000000..434ba47ca1 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt @@ -0,0 +1,116 @@ +// Autogenerated from Pigeon (v26.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.connectivity + +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 ConnectivityPigeonUtils { + + 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) + ) + } + } +} + +/** + * 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() + +enum class NetworkCapability(val raw: Int) { + CELLULAR(0), + WIFI(1), + VPN(2), + UNMETERED(3); + + companion object { + fun ofRaw(raw: Int): NetworkCapability? { + return values().firstOrNull { it.raw == raw } + } + } +} +private open class ConnectivityPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + NetworkCapability.ofRaw(it.toInt()) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is NetworkCapability -> { + stream.write(129) + writeValue(stream, value.raw) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface ConnectivityApi { + fun getCapabilities(): List + + companion object { + /** The codec used by ConnectivityApi. */ + val codec: MessageCodec by lazy { + ConnectivityPigeonCodec() + } + /** Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getCapabilities()) + } catch (exception: Throwable) { + ConnectivityPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt new file mode 100644 index 0000000000..e8554dd63a --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt @@ -0,0 +1,39 @@ +package app.alextran.immich.connectivity + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager + +class ConnectivityApiImpl(context: Context) : ConnectivityApi { + private val connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + + override fun getCapabilities(): List { + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + ?: return emptyList() + + val hasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) + val hasCellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + val hasVpn = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) + val isUnmetered = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + + return buildList { + if (hasWifi) add(NetworkCapability.WIFI) + if (hasCellular) add(NetworkCapability.CELLULAR) + if (hasVpn) { + add(NetworkCapability.VPN) + if (!hasWifi && !hasCellular) { + if (wifiManager.isWifiEnabled) add(NetworkCapability.WIFI) + // If VPN is active, but neither WIFI nor CELLULAR is reported as active, + // assume CELLULAR if WIFI is not enabled + else add(NetworkCapability.CELLULAR) + } + } + if (isUnmetered) add(NetworkCapability.UNMETERED) + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt index 9426ba43dc..a9d602c19c 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt @@ -8,7 +8,6 @@ import android.net.Uri import android.os.Build import android.os.CancellationSignal import android.os.OperationCanceledException -import android.provider.MediaStore import android.provider.MediaStore.Images import android.provider.MediaStore.Video import android.util.Size @@ -18,8 +17,8 @@ import java.util.concurrent.Executors import com.bumptech.glide.Glide import com.bumptech.glide.Priority import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL import java.util.Base64 -import java.util.HashMap import java.util.concurrent.CancellationException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Future @@ -122,15 +121,14 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { signal: CancellationSignal ) { signal.throwIfCanceled() - val targetWidth = width.toInt() - val targetHeight = height.toInt() + val size = Size(width.toInt(), height.toInt()) val id = assetId.toLong() signal.throwIfCanceled() val bitmap = if (isVideo) { - decodeVideoThumbnail(id, targetWidth, targetHeight, signal) + decodeVideoThumbnail(id, size, signal) } else { - decodeImage(id, targetWidth, targetHeight, signal) + decodeImage(id, size, signal) } processBitmap(bitmap, callback, signal) @@ -153,9 +151,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { bitmap.recycle() signal.throwIfCanceled() val res = mapOf( - "pointer" to pointer, - "width" to actualWidth.toLong(), - "height" to actualHeight.toLong() + "pointer" to pointer, "width" to actualWidth.toLong(), "height" to actualHeight.toLong() ) callback(Result.success(res)) } catch (e: Exception) { @@ -164,67 +160,56 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { } } - private fun decodeImage( - id: Long, targetWidth: Int, targetHeight: Int, signal: CancellationSignal - ): Bitmap { + private fun decodeImage(id: Long, size: Size, signal: CancellationSignal): Bitmap { signal.throwIfCanceled() val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id) - if (targetHeight > 768 || targetWidth > 768) { - return decodeSource(uri, targetWidth, targetHeight, signal) + if (size.width <= 0 || size.height <= 0 || size.width > 768 || size.height > 768) { + return decodeSource(uri, size, signal) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal) + resolver.loadThumbnail(uri, size, signal) } else { signal.setOnCancelListener { Images.Thumbnails.cancelThumbnailRequest(resolver, id) } Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, OPTIONS) } } - private fun decodeVideoThumbnail( - id: Long, targetWidth: Int, targetHeight: Int, signal: CancellationSignal - ): Bitmap { + private fun decodeVideoThumbnail(id: Long, target: Size, signal: CancellationSignal): Bitmap { signal.throwIfCanceled() return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id) - resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal) + // ensure a valid resolution as the thumbnail is used for videos even when no scaling is needed + val size = if (target.width > 0 && target.height > 0) target else Size(768, 768) + resolver.loadThumbnail(uri, size, signal) } else { signal.setOnCancelListener { Video.Thumbnails.cancelThumbnailRequest(resolver, id) } Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, OPTIONS) } } - private fun decodeSource( - uri: Uri, targetWidth: Int, targetHeight: Int, signal: CancellationSignal - ): Bitmap { + private fun decodeSource(uri: Uri, target: Size, signal: CancellationSignal): Bitmap { signal.throwIfCanceled() return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val source = ImageDecoder.createSource(resolver, uri) signal.throwIfCanceled() ImageDecoder.decodeBitmap(source) { decoder, info, _ -> - val sampleSize = - getSampleSize(info.size.width, info.size.height, targetWidth, targetHeight) - decoder.setTargetSampleSize(sampleSize) + if (target.width > 0 && target.height > 0) { + val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) + decoder.setTargetSampleSize(sample) + } decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)) } } else { - val ref = Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri) - .disallowHardwareConfig().format(DecodeFormat.PREFER_ARGB_8888) - .submit(targetWidth, targetHeight) + val ref = + Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri).disallowHardwareConfig() + .format(DecodeFormat.PREFER_ARGB_8888).submit( + if (target.width > 0) target.width else SIZE_ORIGINAL, + if (target.height > 0) target.height else SIZE_ORIGINAL, + ) signal.setOnCancelListener { Glide.with(ctx).clear(ref) } ref.get() } } - - private fun getSampleSize(fullWidth: Int, fullHeight: Int, reqWidth: Int, reqHeight: Int): Int { - return 1 shl max( - 0, floor( - min( - log2(fullWidth / reqWidth.toDouble()), - log2(fullHeight / reqHeight.toDouble()), - ) - ).toInt() - ) - } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt index 9c618d9ed0..28400c803f 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -209,6 +209,40 @@ data class SyncDelta ( override fun hashCode(): Int = toList().hashCode() } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class HashResult ( + val assetId: String, + val error: String? = null, + val hash: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): HashResult { + val assetId = pigeonVar_list[0] as String + val error = pigeonVar_list[1] as String? + val hash = pigeonVar_list[2] as String? + return HashResult(assetId, error, hash) + } + } + fun toList(): List { + return listOf( + assetId, + error, + hash, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is HashResult) { + 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) { @@ -227,6 +261,11 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { SyncDelta.fromList(it) } } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { + HashResult.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -244,11 +283,16 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(131) writeValue(stream, value.toList()) } + is HashResult -> { + stream.write(132) + 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 @@ -259,7 +303,8 @@ interface NativeSyncApi { fun getAlbums(): List fun getAssetsCountSince(albumId: String, timestamp: Long): Long fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List - fun hashPaths(paths: List): List + fun hashAssets(assetIds: List, allowNetworkAccess: Boolean, callback: (Result>) -> Unit) + fun cancelHashing() companion object { /** The codec used by NativeSyncApi. */ @@ -402,13 +447,33 @@ interface NativeSyncApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths$separatedMessageChannelSuffix", codec, taskQueue) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$separatedMessageChannelSuffix", codec, taskQueue) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List - val pathsArg = args[0] as List + val assetIdsArg = args[0] as List + val allowNetworkAccessArg = args[1] as Boolean + api.hashAssets(assetIdsArg, allowNetworkAccessArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> val wrapped: List = try { - listOf(api.hashPaths(pathsArg)) + api.cancelHashing() + listOf(null) } catch (exception: Throwable) { MessagesPigeonUtils.wrapError(exception) } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index b2ceb8a9f2..868f3c6cdd 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -1,14 +1,25 @@ package app.alextran.immich.sync import android.annotation.SuppressLint +import android.content.ContentUris import android.content.Context import android.database.Cursor import android.provider.MediaStore -import android.util.Log +import android.util.Base64 import androidx.core.database.getStringOrNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import java.io.File -import java.io.FileInputStream import java.security.MessageDigest +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.coroutineContext sealed class AssetResult { data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult() @@ -19,8 +30,12 @@ sealed class AssetResult { open class NativeSyncApiImplBase(context: Context) { private val ctx: Context = context.applicationContext + private var hashTask: Job? = null + companion object { - private const val TAG = "NativeSyncApiImplBase" + private const val MAX_CONCURRENT_HASH_OPERATIONS = 16 + private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS) + private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED" const val MEDIA_SELECTION = "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" @@ -215,23 +230,74 @@ open class NativeSyncApiImplBase(context: Context) { .toList() } - fun hashPaths(paths: List): List { - val buffer = ByteArray(HASH_BUFFER_SIZE) - val digest = MessageDigest.getInstance("SHA-1") + fun hashAssets( + assetIds: List, + // allowNetworkAccess is only used on the iOS implementation + @Suppress("UNUSED_PARAMETER") allowNetworkAccess: Boolean, + callback: (Result>) -> Unit + ) { + if (assetIds.isEmpty()) { + callback(Result.success(emptyList())) + return + } - return paths.map { path -> + hashTask?.cancel() + hashTask = CoroutineScope(Dispatchers.IO).launch { try { - FileInputStream(path).use { file -> - var bytesRead: Int - while (file.read(buffer).also { bytesRead = it } > 0) { - digest.update(buffer, 0, bytesRead) + val results = assetIds.map { assetId -> + async { + hashSemaphore.withPermit { + ensureActive() + hashAsset(assetId) + } } - } - digest.digest() + }.awaitAll() + + callback(Result.success(results)) + } catch (e: CancellationException) { + callback( + Result.failure( + FlutterError( + HASHING_CANCELLED_CODE, + "Hashing operation was cancelled", + null + ) + ) + ) } catch (e: Exception) { - Log.w(TAG, "Failed to hash file $path: $e") - null + callback(Result.failure(e)) } } } + + private suspend fun hashAsset(assetId: String): HashResult { + return try { + val assetUri = ContentUris.withAppendedId( + MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), + assetId.toLong() + ) + + val digest = MessageDigest.getInstance("SHA-1") + ctx.contentResolver.openInputStream(assetUri)?.use { inputStream -> + var bytesRead: Int + val buffer = ByteArray(HASH_BUFFER_SIZE) + while (inputStream.read(buffer).also { bytesRead = it } > 0) { + coroutineContext.ensureActive() + digest.update(buffer, 0, bytesRead) + } + } ?: return HashResult(assetId, "Cannot open input stream for asset", null) + + val hashString = Base64.encodeToString(digest.digest(), Base64.NO_WRAP) + HashResult(assetId, null, hashString) + } catch (e: SecurityException) { + HashResult(assetId, "Permission denied accessing asset: ${e.message}", null) + } catch (e: Exception) { + HashResult(assetId, "Failed to hash asset: ${e.message}", null) + } + } + + fun cancelHashing() { + hashTask?.cancel() + hashTask = null + } } diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index bcf3daa1c8..719c946bd6 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,5 +1,5 @@ allprojects { - ext.kotlin_version = '2.0.20' + ext.kotlin_version = '2.2.20' repositories { google() @@ -16,8 +16,8 @@ subprojects { if (project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library")) { project.android { - compileSdkVersion 35 - buildToolsVersion "35.0.0" + compileSdkVersion 36 + buildToolsVersion "36.0.0" } } } diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 1be349f808..f359e3b614 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" => 3009, - "android.injected.version.name" => "1.139.4", + "android.injected.version.code" => 3020, + "android.injected.version.name" => "2.0.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/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index dedd5d1e69..ed4c299adb 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 29c3a7c056..fbed55a3e3 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -18,10 +18,10 @@ pluginManagement { 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.android.application" version '8.11.2' apply false + id "org.jetbrains.kotlin.android" version "2.2.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 + id 'com.google.devtools.ksp' version '2.2.20-2.0.3' apply false } include ":app" diff --git a/mobile/drift_schemas/main/drift_schema_v10.json b/mobile/drift_schemas/main/drift_schema_v10.json new file mode 100644 index 0000000000..aba030da04 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v10.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","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"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","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":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_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":"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":"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":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","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":4,"references":[0,1],"type":"table","data":{"name":"remote_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":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"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":"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":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":6,"references":[3,5],"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":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":12,"references":[],"type":"table","data":{"name":"auth_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":"email","getter_name":"email","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":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","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":[]},{"name":"pin_code","getter_name":"pinCode","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":["id"]}},{"id":13,"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":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":14,"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":15,"references":[1],"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":"double","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":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","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":"lens","getter_name":"lens","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":16,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_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":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_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":17,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":18,"references":[0],"type":"table","data":{"name":"memory_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":"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":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"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":"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(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":19,"references":[1,18],"type":"table","data":{"name":"memory_asset_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":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_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","memory_id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"person_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":"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":"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":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","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":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":21,"references":[1,20],"type":"table","data":{"name":"asset_face_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":"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":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":22,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":23,"references":[15],"type":"index","data":{"on":15,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v11.json b/mobile/drift_schemas/main/drift_schema_v11.json new file mode 100644 index 0000000000..1c100ab37f --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v11.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","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"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","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":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_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":"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":"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":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","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":4,"references":[0,1],"type":"table","data":{"name":"remote_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":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"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":"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":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":6,"references":[3,5],"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"]},{"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":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":12,"references":[],"type":"table","data":{"name":"auth_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":"email","getter_name":"email","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":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","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":[]},{"name":"pin_code","getter_name":"pinCode","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":["id"]}},{"id":13,"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":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":14,"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":15,"references":[1],"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":"double","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":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","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":"lens","getter_name":"lens","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":16,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_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":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_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":17,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":18,"references":[0],"type":"table","data":{"name":"memory_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":"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":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"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":"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(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":19,"references":[1,18],"type":"table","data":{"name":"memory_asset_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":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_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","memory_id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"person_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":"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":"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":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","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":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":21,"references":[1,20],"type":"table","data":{"name":"asset_face_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":"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":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":22,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":23,"references":[15],"type":"index","data":{"on":15,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v12.json b/mobile/drift_schemas/main/drift_schema_v12.json new file mode 100644 index 0000000000..1c100ab37f --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v12.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","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"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","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":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_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":"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":"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":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","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":4,"references":[0,1],"type":"table","data":{"name":"remote_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":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"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":"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":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":6,"references":[3,5],"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"]},{"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":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":12,"references":[],"type":"table","data":{"name":"auth_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":"email","getter_name":"email","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":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","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":[]},{"name":"pin_code","getter_name":"pinCode","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":["id"]}},{"id":13,"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":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":14,"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":15,"references":[1],"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":"double","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":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","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":"lens","getter_name":"lens","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":16,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_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":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_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":17,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":18,"references":[0],"type":"table","data":{"name":"memory_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":"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":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"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":"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(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":19,"references":[1,18],"type":"table","data":{"name":"memory_asset_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":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_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","memory_id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"person_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":"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":"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":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","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":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":21,"references":[1,20],"type":"table","data":{"name":"asset_face_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":"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":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":22,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":23,"references":[15],"type":"index","data":{"on":15,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v9.json b/mobile/drift_schemas/main/drift_schema_v9.json new file mode 100644 index 0000000000..5b08a752ec --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v9.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","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"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","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":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_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":"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":"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":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","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":4,"references":[0,1],"type":"table","data":{"name":"remote_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":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"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":"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":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":6,"references":[3,5],"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":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":12,"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":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":13,"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":14,"references":[1],"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":"double","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":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","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":"lens","getter_name":"lens","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":15,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_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":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_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":16,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"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":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"memory_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":"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":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"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":"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(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":18,"references":[1,17],"type":"table","data":{"name":"memory_asset_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":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_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","memory_id"]}},{"id":19,"references":[0],"type":"table","data":{"name":"person_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":"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":"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":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","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":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":20,"references":[1,19],"type":"table","data":{"name":"asset_face_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":"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":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":21,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":22,"references":[14],"type":"index","data":{"on":14,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore index f312f249a3..e32cadbf68 100644 --- a/mobile/ios/.gitignore +++ b/mobile/ios/.gitignore @@ -4,7 +4,6 @@ *.moved-aside *.pbxuser *.perspectivev3 -**/*sync/ .sconsign.dblite .tags* **/.vagrant/ diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist index 7c56964006..1dc6cf7652 100644 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 09bd36022b..502fd9008f 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -253,7 +253,7 @@ SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 563c4cda33..6403a0ab4b 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,11 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; }; + B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; }; + B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; }; + B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; }; + B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; }; @@ -92,6 +97,11 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; + B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; + B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; + B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = ""; }; + B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = ""; }; + B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = ""; }; E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; @@ -237,6 +247,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B25D37792E72CA15008B6CA7 /* Connectivity */, + B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, 65DD438629917FAD0047FFA8 /* BackgroundSync */, @@ -254,6 +266,25 @@ path = Runner; sourceTree = ""; }; + B21E34A62E5AF9760031FDB9 /* Background */ = { + isa = PBXGroup; + children = ( + B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */, + B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */, + B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */, + ); + path = Background; + sourceTree = ""; + }; + B25D37792E72CA15008B6CA7 /* Connectivity */ = { + isa = PBXGroup; + children = ( + B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */, + B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */, + ); + path = Connectivity; + sourceTree = ""; + }; FAC6F8B62D287F120078CB2F /* ShareExtension */ = { isa = PBXGroup; children = ( @@ -540,10 +571,15 @@ files = ( 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, + B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */, FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */, + B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */, FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */, + B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -669,7 +705,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -813,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -843,7 +879,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -877,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -920,7 +956,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -960,7 +996,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -999,7 +1035,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1043,7 +1079,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1084,7 +1120,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 230; 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 dedda5bd12..3476030923 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -19,13 +19,12 @@ import UIKit } GeneratedPluginRegistrant.register(with: self) - BackgroundServicePlugin.registerBackgroundProcessing() - - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl()) - ThumbnailApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ThumbnailApiImpl()) + AppDelegate.registerPlugins(binaryMessenger: controller.binaryMessenger) + BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) + + BackgroundServicePlugin.registerBackgroundProcessing() + BackgroundWorkerApiImpl.registerBackgroundWorkers() BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { @@ -51,4 +50,10 @@ import UIKit return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + public static func registerPlugins(binaryMessenger: FlutterBinaryMessenger) { + NativeSyncApiSetup.setUp(binaryMessenger: binaryMessenger, api: NativeSyncApiImpl()) + ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailApiImpl()) + BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl()) + } } diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift new file mode 100644 index 0000000000..ece5cd5f64 --- /dev/null +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -0,0 +1,365 @@ +// Autogenerated from Pigeon (v26.0.0), 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 + +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 createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + +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 deepEqualsBackgroundWorker(_ 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 !deepEqualsBackgroundWorker(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 !deepEqualsBackgroundWorker(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 deepHashBackgroundWorker(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashBackgroundWorker(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashBackgroundWorker(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 BackgroundWorkerSettings: Hashable { + var requiresCharging: Bool + var minimumDelaySeconds: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> BackgroundWorkerSettings? { + let requiresCharging = pigeonVar_list[0] as! Bool + let minimumDelaySeconds = pigeonVar_list[1] as! Int64 + + return BackgroundWorkerSettings( + requiresCharging: requiresCharging, + minimumDelaySeconds: minimumDelaySeconds + ) + } + func toList() -> [Any?] { + return [ + requiresCharging, + minimumDelaySeconds, + ] + } + static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool { + return deepEqualsBackgroundWorker(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBackgroundWorker(value: toList(), hasher: &hasher) + } +} + +private class BackgroundWorkerPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return BackgroundWorkerSettings.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class BackgroundWorkerPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? BackgroundWorkerSettings { + super.writeByte(129) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class BackgroundWorkerPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return BackgroundWorkerPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return BackgroundWorkerPigeonCodecWriter(data: data) + } +} + +class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol BackgroundWorkerFgHostApi { + func enable() throws + func configure(settings: BackgroundWorkerSettings) throws + func disable() throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class BackgroundWorkerFgHostApiSetup { + static var codec: FlutterStandardMessageCodec { BackgroundWorkerPigeonCodec.shared } + /// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + enableChannel.setMessageHandler { _, reply in + do { + try api.enable() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + enableChannel.setMessageHandler(nil) + } + let configureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + configureChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let settingsArg = args[0] as! BackgroundWorkerSettings + do { + try api.configure(settings: settingsArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + configureChannel.setMessageHandler(nil) + } + let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + disableChannel.setMessageHandler { _, reply in + do { + try api.disable() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + disableChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol BackgroundWorkerBgHostApi { + func onInitialized() throws + func showNotification(title: String, content: String) throws + func close() throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class BackgroundWorkerBgHostApiSetup { + static var codec: FlutterStandardMessageCodec { BackgroundWorkerPigeonCodec.shared } + /// Sets up an instance of `BackgroundWorkerBgHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerBgHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let onInitializedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + onInitializedChannel.setMessageHandler { _, reply in + do { + try api.onInitialized() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + onInitializedChannel.setMessageHandler(nil) + } + let showNotificationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + showNotificationChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let titleArg = args[0] as! String + let contentArg = args[1] as! String + do { + try api.showNotification(title: titleArg, content: contentArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + showNotificationChannel.setMessageHandler(nil) + } + let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + closeChannel.setMessageHandler { _, reply in + do { + try api.close() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + closeChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol BackgroundWorkerFlutterApiProtocol { + func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) + func onAndroidUpload(completion: @escaping (Result) -> Void) + func cancel(completion: @escaping (Result) -> Void) +} +class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: BackgroundWorkerPigeonCodec { + return BackgroundWorkerPigeonCodec.shared + } + func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([isRefreshArg, maxSecondsArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onAndroidUpload(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func cancel(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } +} diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift new file mode 100644 index 0000000000..c3268b4a2b --- /dev/null +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -0,0 +1,179 @@ +import BackgroundTasks +import Flutter + +enum BackgroundTaskType { case refresh, processing } + +/* + * DEBUG: Testing Background Tasks in Xcode + * + * To test background task functionality during development: + * 1. Pause the application in Xcode debugger + * 2. In the debugger console, enter one of the following commands: + + ## For background refresh (short-running sync): + + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"] + + ## For background processing (long-running upload): + + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"] + + * To simulate task expiration (useful for testing expiration handlers): + + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"] + + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"] + + * 3. Resume the application to see the background code execute + * + * NOTE: This must be tested on a physical device, not in the simulator. + * In testing, only the background processing task can be reliably simulated. + * These commands submit the respective task to BGTaskScheduler for immediate processing. + * Use the expiration commands to test how the app handles iOS terminating background tasks. + */ + + +/// The background worker which creates a new Flutter VM, communicates with it +/// to run the backup job, and then finishes execution and calls back to its callback handler. +/// This class manages a separate Flutter engine instance for background execution, +/// independent of the main UI Flutter engine. +class BackgroundWorker: BackgroundWorkerBgHostApi { + private let taskType: BackgroundTaskType + /// The maximum number of seconds to run the task before timing out + private let maxSeconds: Int? + /// Callback function to invoke when the background task completes + private let completionHandler: (_ success: Bool) -> Void + + /// The Flutter engine created specifically for background execution. + /// This is a separate instance from the main Flutter engine that handles the UI. + /// It operates in its own isolate and doesn't share memory with the main engine. + /// Must be properly started, registered, and torn down during background execution. + private let engine = FlutterEngine(name: "BackgroundImmich") + + /// Used to call methods on the flutter side + private var flutterApi: BackgroundWorkerFlutterApi? + + /// Flag to track whether the background task has completed to prevent duplicate completions + private var isComplete = false + + /** + * Initializes a new background worker with the specified task type and execution constraints. + * Creates a new Flutter engine instance for background execution and sets up the necessary + * communication channels between native iOS and Flutter code. + * + * - Parameters: + * - taskType: The type of background task to execute (upload or sync task) + * - maxSeconds: Optional maximum execution time in seconds before the task is cancelled + * - completionHandler: Callback function invoked when the task completes, with success status + */ + init(taskType: BackgroundTaskType, maxSeconds: Int?, completionHandler: @escaping (_ success: Bool) -> Void) { + self.taskType = taskType + self.maxSeconds = maxSeconds + self.completionHandler = completionHandler + // Should be initialized only after the engine starts running + self.flutterApi = nil + } + + /** + * Starts the background Flutter engine and begins execution of the background task. + * Retrieves the callback handle from UserDefaults, looks up the Flutter callback, + * starts the engine, and sets up a timeout timer if specified. + */ + func run() { + // Start the Flutter engine with the specified callback as the entry point + let isRunning = engine.run( + withEntrypoint: "backgroundSyncNativeEntrypoint", + libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart" + ) + + // Verify that the Flutter engine started successfully + if !isRunning { + complete(success: false) + return + } + + // Register plugins in the new engine + GeneratedPluginRegistrant.register(with: engine) + // Register custom plugins + AppDelegate.registerPlugins(binaryMessenger: engine.binaryMessenger) + flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger) + BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self) + + // Set up a timeout timer if maxSeconds was specified to prevent runaway background tasks + if maxSeconds != nil { + // Schedule a timer to cancel the task after the specified timeout period + Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in + self.close() + } + } + } + + /** + * Called by the Flutter side when it has finished initialization and is ready to receive commands. + * Routes the appropriate task type (refresh or processing) to the corresponding Flutter method. + * This method acts as a bridge between the native iOS background task system and Flutter. + */ + func onInitialized() throws { + flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in + self.handleHostResult(result: result) + }) + } + + func showNotification(title: String, content: String) throws { + // No-op on iOS for the time being + } + + /** + * Cancels the currently running background task, either due to timeout or external request. + * Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure + * the completion handler is eventually called even if Flutter doesn't respond. + */ + func close() { + if isComplete { + return + } + + flutterApi?.cancel { result in + self.complete(success: false) + } + + // Fallback safety mechanism: ensure completion is called within 2 seconds + // This prevents the background task from hanging indefinitely if Flutter doesn't respond + Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + self.complete(success: false) + } + } + + + /** + * Handles the result from Flutter API calls and determines the success/failure status. + * Converts Flutter's Result type to a simple boolean success indicator for task completion. + * + * - Parameter result: The result returned from a Flutter API call + */ + private func handleHostResult(result: Result) { + switch result { + case .success(): self.complete(success: true) + case .failure(_): self.close() + } + } + + /** + * Cleans up resources by destroying the Flutter engine context and invokes the completion handler. + * This method ensures that the background task is marked as complete, releases the Flutter engine, + * and notifies the caller of the task's success or failure. This is the final step in the + * background task lifecycle and should only be called once per task instance. + * + * - Parameter success: Indicates whether the background task completed successfully + */ + private func complete(success: Bool) { + if(isComplete) { + return + } + + isComplete = true + engine.destroyContext() + flutterApi = nil + completionHandler(success) + } +} diff --git a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift new file mode 100644 index 0000000000..f7f8f69989 --- /dev/null +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -0,0 +1,123 @@ +import BackgroundTasks + +class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { + + func enable() throws { + BackgroundWorkerApiImpl.scheduleRefreshWorker() + BackgroundWorkerApiImpl.scheduleProcessingWorker() + print("BackgroundWorkerApiImpl:enable Background worker scheduled") + } + + func configure(settings: BackgroundWorkerSettings) throws { + // Android only + } + + func disable() throws { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); + print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers") + } + + private static let refreshTaskID = "app.alextran.immich.background.refreshUpload" + private static let processingTaskID = "app.alextran.immich.background.processingUpload" + private static let taskSemaphore = DispatchSemaphore(value: 1) + + public static func registerBackgroundWorkers() { + BGTaskScheduler.shared.register( + forTaskWithIdentifier: processingTaskID, using: nil) { task in + if task is BGProcessingTask { + handleBackgroundProcessing(task: task as! BGProcessingTask) + } + } + + BGTaskScheduler.shared.register( + forTaskWithIdentifier: refreshTaskID, using: nil) { task in + if task is BGAppRefreshTask { + handleBackgroundRefresh(task: task as! BGAppRefreshTask) + } + } + } + + private static func scheduleRefreshWorker() { + let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID) + backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins + + do { + try BGTaskScheduler.shared.submit(backgroundRefresh) + } catch { + print("Could not schedule the refresh upload task \(error.localizedDescription)") + } + } + + private static func scheduleProcessingWorker() { + let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID) + + backgroundProcessing.requiresNetworkConnectivity = true + backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins + + do { + try BGTaskScheduler.shared.submit(backgroundProcessing) + } catch { + print("Could not schedule the processing upload task \(error.localizedDescription)") + } + } + + private static func handleBackgroundRefresh(task: BGAppRefreshTask) { + scheduleRefreshWorker() + // If another task is running, cede the background time back to the OS + if taskSemaphore.wait(timeout: .now()) == .success { + // Restrict the refresh task to run only for a maximum of (maxSeconds) seconds + runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20) + } else { + task.setTaskCompleted(success: false) + } + } + + private static func handleBackgroundProcessing(task: BGProcessingTask) { + scheduleProcessingWorker() + taskSemaphore.wait() + // There are no restrictions for processing tasks. Although, the OS could signal expiration at any time + runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil) + } + + /** + * Executes the background worker within the context of a background task. + * This method creates a BackgroundWorker, sets up task expiration handling, + * and manages the synchronization between the background task and the Flutter engine. + * + * - Parameters: + * - task: The iOS background task that provides the execution context + * - taskType: The type of background operation to perform (refresh or processing) + * - maxSeconds: Optional timeout for the operation in seconds + */ + private static func runBackgroundWorker(task: BGTask, taskType: BackgroundTaskType, maxSeconds: Int?) { + defer { taskSemaphore.signal() } + let semaphore = DispatchSemaphore(value: 0) + var isSuccess = true + + let backgroundWorker = BackgroundWorker(taskType: taskType, maxSeconds: maxSeconds) { success in + isSuccess = success + semaphore.signal() + } + + task.expirationHandler = { + DispatchQueue.main.async { + backgroundWorker.close() + } + isSuccess = false + + // Schedule a timer to signal the semaphore after 2 seconds + Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + semaphore.signal() + } + } + + DispatchQueue.main.async { + backgroundWorker.run() + } + + semaphore.wait() + task.setTaskCompleted(success: isSuccess) + print("Background task completed with success: \(isSuccess)") + } +} diff --git a/mobile/ios/Runner/Connectivity/Connectivity.g.swift b/mobile/ios/Runner/Connectivity/Connectivity.g.swift new file mode 100644 index 0000000000..45333f03d8 --- /dev/null +++ b/mobile/ios/Runner/Connectivity/Connectivity.g.swift @@ -0,0 +1,129 @@ +// Autogenerated from Pigeon (v26.0.0), 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 + +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? +} + + +enum NetworkCapability: Int { + case cellular = 0 + case wifi = 1 + case vpn = 2 + case unmetered = 3 +} + +private class ConnectivityPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return NetworkCapability(rawValue: enumResultAsInt) + } + return nil + default: + return super.readValue(ofType: type) + } + } +} + +private class ConnectivityPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? NetworkCapability { + super.writeByte(129) + super.writeValue(value.rawValue) + } else { + super.writeValue(value) + } + } +} + +private class ConnectivityPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return ConnectivityPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return ConnectivityPigeonCodecWriter(data: data) + } +} + +class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol ConnectivityApi { + func getCapabilities() throws -> [NetworkCapability] +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class ConnectivityApiSetup { + static var codec: FlutterStandardMessageCodec { ConnectivityPigeonCodec.shared } + /// Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + #if os(iOS) + let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() + #else + let taskQueue: FlutterTaskQueue? = nil + #endif + let getCapabilitiesChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getCapabilitiesChannel.setMessageHandler { _, reply in + do { + let result = try api.getCapabilities() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getCapabilitiesChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift new file mode 100644 index 0000000000..0261cb26fb --- /dev/null +++ b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift @@ -0,0 +1,6 @@ + +class ConnectivityApiImpl: ConnectivityApi { + func getCapabilities() throws -> [NetworkCapability] { + [] + } +} diff --git a/mobile/ios/Runner/Images/ThumbnailsImpl.swift b/mobile/ios/Runner/Images/ThumbnailsImpl.swift index 42f0930cb1..452ca62377 100644 --- a/mobile/ios/Runner/Images/ThumbnailsImpl.swift +++ b/mobile/ios/Runner/Images/ThumbnailsImpl.swift @@ -46,6 +46,23 @@ class ThumbnailApiImpl: ThumbnailApi { assetCache.countLimit = 10000 return assetCache }() + private static let activitySemaphore = DispatchSemaphore(value: 1) + private static let willResignActiveObserver = NotificationCenter.default.addObserver( + forName: UIApplication.willResignActiveNotification, + object: nil, + queue: .main + ) { _ in + processingQueue.suspend() + activitySemaphore.wait() + } + private static let didBecomeActiveObserver = NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { _ in + processingQueue.resume() + activitySemaphore.signal() + } func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) { Self.processingQueue.async { @@ -53,6 +70,7 @@ class ThumbnailApiImpl: ThumbnailApi { else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))} let (width, height, pointer) = thumbHashToRGBA(hash: data) + self.waitForActiveState() completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)])) } } @@ -87,7 +105,7 @@ class ThumbnailApiImpl: ThumbnailApi { var image: UIImage? Self.imageManager.requestImage( for: asset, - targetSize: CGSize(width: Double(width), height: Double(height)), + targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize, contentMode: .aspectFill, options: Self.requestOptions, resultHandler: { (_image, info) -> Void in @@ -142,6 +160,7 @@ class ThumbnailApiImpl: ThumbnailApi { return completion(Self.cancelledResult) } + self.waitForActiveState() completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)])) Self.removeRequest(requestId: requestId) } @@ -184,4 +203,9 @@ class ThumbnailApiImpl: ThumbnailApi { assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } return asset } + + func waitForActiveState() { + Self.activitySemaphore.wait() + Self.activitySemaphore.signal() + } } diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 8c72f125f4..e9912f260c 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -1,187 +1,189 @@ - - 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.139.3 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - Share Extension - CFBundleURLSchemes - - ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - - - - CFBundleTypeRole - Editor - CFBundleURLName - Deep Link - CFBundleURLSchemes - - immich - - - - CFBundleVersion - 217 - FLTEnableImpeller - - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - https - - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - No - MGLMapboxMetricsEnabledSettingShownInApp - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _googlecast._tcp - _CC1AD845._googlecast._tcp - - NSCameraUsageDescription - We need to access the camera to let you take beautiful video using this app - NSFaceIDUsageDescription - We need to use FaceID to allow access to your locked folder - 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 - - NSLocalNetworkUsageDescription - We need local network permission to connect to the local server using IP address and + + AppGroupId + $(CUSTOM_GROUP_ID) + BGTaskSchedulerPermittedIdentifiers + + app.alextran.immich.background.refreshUpload + app.alextran.immich.background.processingUpload + 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 + 2.0.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + Share Extension + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + + CFBundleTypeRole + Editor + CFBundleURLName + Deep Link + CFBundleURLSchemes + + immich + + + + CFBundleVersion + 230 + FLTEnableImpeller + + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + https + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + No + MGLMapboxMetricsEnabledSettingShownInApp + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _googlecast._tcp + _CC1AD845._googlecast._tcp + + NSCameraUsageDescription + We need to access the camera to let you take beautiful video using this app + NSFaceIDUsageDescription + We need to use FaceID to allow access to your locked folder + NSLocalNetworkUsageDescription + We need local network permission to connect to the local server using IP address and allow the casting feature to work - + 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 + + diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index 19f4384672..305aca5266 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -267,6 +267,39 @@ struct SyncDelta: Hashable { } } +/// Generated class from Pigeon that represents data sent in messages. +struct HashResult: Hashable { + var assetId: String + var error: String? = nil + var hash: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> HashResult? { + let assetId = pigeonVar_list[0] as! String + let error: String? = nilOrValue(pigeonVar_list[1]) + let hash: String? = nilOrValue(pigeonVar_list[2]) + + return HashResult( + assetId: assetId, + error: error, + hash: hash + ) + } + func toList() -> [Any?] { + return [ + assetId, + error, + hash, + ] + } + static func == (lhs: HashResult, rhs: HashResult) -> 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 { @@ -276,6 +309,8 @@ private class MessagesPigeonCodecReader: FlutterStandardReader { return PlatformAlbum.fromList(self.readValue() as! [Any?]) case 131: return SyncDelta.fromList(self.readValue() as! [Any?]) + case 132: + return HashResult.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -293,6 +328,9 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter { } else if let value = value as? SyncDelta { super.writeByte(131) super.writeValue(value.toList()) + } else if let value = value as? HashResult { + super.writeByte(132) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -313,6 +351,7 @@ 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 @@ -323,7 +362,8 @@ protocol NativeSyncApi { func getAlbums() throws -> [PlatformAlbum] func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] - func hashPaths(paths: [String]) throws -> [FlutterStandardTypedData?] + func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) + func cancelHashing() throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -459,22 +499,38 @@ class NativeSyncApiSetup { } else { getAssetsForAlbumChannel.setMessageHandler(nil) } - let hashPathsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + let hashAssetsChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) if let api = api { - hashPathsChannel.setMessageHandler { message, reply in + hashAssetsChannel.setMessageHandler { message, reply in let args = message as! [Any?] - let pathsArg = args[0] as! [String] + let assetIdsArg = args[0] as! [String] + let allowNetworkAccessArg = args[1] as! Bool + api.hashAssets(assetIds: assetIdsArg, allowNetworkAccess: allowNetworkAccessArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + hashAssetsChannel.setMessageHandler(nil) + } + let cancelHashingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelHashingChannel.setMessageHandler { _, reply in do { - let result = try api.hashPaths(paths: pathsArg) - reply(wrapResult(result)) + try api.cancelHashing() + reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - hashPathsChannel.setMessageHandler(nil) + cancelHashingChannel.setMessageHandler(nil) } } } diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index 2810dee7c1..bb23bae6b6 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -17,30 +17,16 @@ struct AssetWrapper: Hashable, Equatable { } } -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) }, - width: Int64(pixelWidth), - height: Int64(pixelHeight), - durationInSeconds: Int64(duration), - orientation: 0, - isFavorite: isFavorite - ) - } -} - class NativeSyncApiImpl: NativeSyncApi { private let defaults: UserDefaults private let changeTokenKey = "immich:changeToken" private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] private let recoveredAlbumSubType = 1000000219 - private let hashBufferSize = 2 * 1024 * 1024 + private var hashTask: Task? + private static let hashCancelledCode = "HASH_CANCELLED" + private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil)) + init(with defaults: UserDefaults = .standard) { self.defaults = defaults @@ -96,7 +82,7 @@ class NativeSyncApiImpl: NativeSyncApi { let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) for i in 0.. %@ OR modificationDate > %@", date, date) } - + let result = PHAsset.fetchAssets(in: album, options: options) if(result.count == 0) { return [] @@ -267,23 +253,114 @@ class NativeSyncApiImpl: NativeSyncApi { return assets } - func hashPaths(paths: [String]) throws -> [FlutterStandardTypedData?] { - return paths.map { path in - guard let file = FileHandle(forReadingAtPath: path) else { - print("Cannot open file: \(path)") - return nil - } - - var hasher = Insecure.SHA1() - while autoreleasepool(invoking: { - let chunk = file.readData(ofLength: hashBufferSize) - guard !chunk.isEmpty else { return false } - hasher.update(data: chunk) - return true - }) { } - - let digest = hasher.finalize() - return FlutterStandardTypedData(bytes: Data(digest)) + func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) { + if let prevTask = hashTask { + prevTask.cancel() + hashTask = nil + } + hashTask = Task { [weak self] in + var missingAssetIds = Set(assetIds) + var assets = [PHAsset]() + assets.reserveCapacity(assetIds.count) + PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil).enumerateObjects { (asset, _, stop) in + if Task.isCancelled { + stop.pointee = true + return + } + missingAssetIds.remove(asset.localIdentifier) + assets.append(asset) } + + if Task.isCancelled { + return completion(Self.hashCancelled) + } + + await withTaskGroup(of: HashResult?.self) { taskGroup in + var results = [HashResult]() + results.reserveCapacity(assets.count) + for asset in assets { + if Task.isCancelled { + return completion(Self.hashCancelled) + } + taskGroup.addTask { + guard let self = self else { return nil } + return await self.hashAsset(asset, allowNetworkAccess: allowNetworkAccess) + } + } + + for await result in taskGroup { + guard let result = result else { + return completion(Self.hashCancelled) + } + results.append(result) + } + + for missing in missingAssetIds { + results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil)) + } + + completion(.success(results)) + } + } + } + + func cancelHashing() { + hashTask?.cancel() + hashTask = nil + } + + private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? { + class RequestRef { + var id: PHAssetResourceDataRequestID? + } + let requestRef = RequestRef() + return await withTaskCancellationHandler(operation: { + if Task.isCancelled { + return nil + } + + guard let resource = asset.getResource() else { + return HashResult(assetId: asset.localIdentifier, error: "Cannot get asset resource", hash: nil) + } + + if Task.isCancelled { + return nil + } + + let options = PHAssetResourceRequestOptions() + options.isNetworkAccessAllowed = allowNetworkAccess + + return await withCheckedContinuation { continuation in + var hasher = Insecure.SHA1() + + requestRef.id = PHAssetResourceManager.default().requestData( + for: resource, + options: options, + dataReceivedHandler: { data in + hasher.update(data: data) + }, + completionHandler: { error in + let result: HashResult? = switch (error) { + case let e as PHPhotosError where e.code == .userCancelled: nil + case let .some(e): HashResult( + assetId: asset.localIdentifier, + error: "Failed to hash asset: \(e.localizedDescription)", + hash: nil + ) + case .none: + HashResult( + assetId: asset.localIdentifier, + error: nil, + hash: Data(hasher.finalize()).base64EncodedString() + ) + } + continuation.resume(returning: result) + } + ) + } + }, onCancel: { + guard let requestId = requestRef.id else { return } + PHAssetResourceManager.default().cancelDataRequest(requestId) + }) } } diff --git a/mobile/ios/Runner/Sync/PHAssetExtensions.swift b/mobile/ios/Runner/Sync/PHAssetExtensions.swift new file mode 100644 index 0000000000..2b1ef6ac88 --- /dev/null +++ b/mobile/ios/Runner/Sync/PHAssetExtensions.swift @@ -0,0 +1,77 @@ +import Photos + +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) }, + width: Int64(pixelWidth), + height: Int64(pixelHeight), + durationInSeconds: Int64(duration), + orientation: 0, + isFavorite: isFavorite + ) + } + + var title: String { + return filename ?? originalFilename ?? "" + } + + var filename: String? { + return value(forKey: "filename") as? String + } + + // This method is expected to be slow as it goes through the asset resources to fetch the originalFilename + var originalFilename: String? { + return getResource()?.originalFilename + } + + func getResource() -> PHAssetResource? { + let resources = PHAssetResource.assetResources(for: self) + + let filteredResources = resources.filter { $0.isMediaResource && isValidResourceType($0.type) } + + guard !filteredResources.isEmpty else { + return nil + } + + if filteredResources.count == 1 { + return filteredResources.first + } + + if let currentResource = filteredResources.first(where: { $0.isCurrent }) { + return currentResource + } + + if let fullSizeResource = filteredResources.first(where: { isFullSizeResourceType($0.type) }) { + return fullSizeResource + } + + return nil + } + + private func isValidResourceType(_ type: PHAssetResourceType) -> Bool { + switch mediaType { + case .image: + return [.photo, .alternatePhoto, .fullSizePhoto].contains(type) + case .video: + return [.video, .fullSizeVideo, .fullSizePairedVideo].contains(type) + default: + return false + } + } + + private func isFullSizeResourceType(_ type: PHAssetResourceType) -> Bool { + switch mediaType { + case .image: + return type == .fullSizePhoto + case .video: + return type == .fullSizeVideo + default: + return false + } + } +} diff --git a/mobile/ios/Runner/Sync/PHAssetResourceExtensions.swift b/mobile/ios/Runner/Sync/PHAssetResourceExtensions.swift new file mode 100644 index 0000000000..699d55a98d --- /dev/null +++ b/mobile/ios/Runner/Sync/PHAssetResourceExtensions.swift @@ -0,0 +1,16 @@ + +import Photos + +extension PHAssetResource { + var isCurrent: Bool { + return value(forKey: "isCurrent") as? Bool ?? false + } + + var isMediaResource: Bool { + var isMedia = type != .adjustmentData + if #available(iOS 17, *) { + isMedia = isMedia && type != .photoProxy + } + return isMedia + } +} diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 60a5f24eef..71d56a98f9 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.139.4" + version_number: "2.0.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 0cfc0c57e3..7429616f14 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + const int noDbId = -9223372036854775808; // from Isar const double downloadCompleted = -1; const double downloadFailed = -2; @@ -10,7 +12,7 @@ const int kSyncEventBatchSize = 5000; const int kFetchLocalAssetsBatchSize = 40000; // Hash batch limits -const int kBatchHashFileLimit = 256; +final int kBatchHashFileLimit = Platform.isIOS ? 32 : 512; const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB // Secure storage keys @@ -45,3 +47,5 @@ const List<(String, String)> kWidgetNames = [ const double kUploadStatusFailed = -1.0; const double kUploadStatusCanceled = -2.0; + +const int kMinMonthsToEnableScrubberSnap = 12; diff --git a/mobile/lib/domain/models/album/local_album.model.dart b/mobile/lib/domain/models/album/local_album.model.dart index b0b08937ab..ea06118aa1 100644 --- a/mobile/lib/domain/models/album/local_album.model.dart +++ b/mobile/lib/domain/models/album/local_album.model.dart @@ -15,6 +15,7 @@ class LocalAlbum { final int assetCount; final BackupSelection backupSelection; + final String? linkedRemoteAlbumId; const LocalAlbum({ required this.id, @@ -23,6 +24,7 @@ class LocalAlbum { this.assetCount = 0, this.backupSelection = BackupSelection.none, this.isIosSharedAlbum = false, + this.linkedRemoteAlbumId, }); LocalAlbum copyWith({ @@ -32,6 +34,7 @@ class LocalAlbum { int? assetCount, BackupSelection? backupSelection, bool? isIosSharedAlbum, + String? linkedRemoteAlbumId, }) { return LocalAlbum( id: id ?? this.id, @@ -40,6 +43,7 @@ class LocalAlbum { assetCount: assetCount ?? this.assetCount, backupSelection: backupSelection ?? this.backupSelection, isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, ); } @@ -53,7 +57,8 @@ class LocalAlbum { other.updatedAt == updatedAt && other.assetCount == assetCount && other.backupSelection == backupSelection && - other.isIosSharedAlbum == isIosSharedAlbum; + other.isIosSharedAlbum == isIosSharedAlbum && + other.linkedRemoteAlbumId == linkedRemoteAlbumId; } @override @@ -63,7 +68,8 @@ class LocalAlbum { updatedAt.hashCode ^ assetCount.hashCode ^ backupSelection.hashCode ^ - isIosSharedAlbum.hashCode; + isIosSharedAlbum.hashCode ^ + linkedRemoteAlbumId.hashCode; } @override @@ -75,6 +81,7 @@ updatedAt: $updatedAt, assetCount: $assetCount, backupSelection: $backupSelection, isIosSharedAlbum: $isIosSharedAlbum +linkedRemoteAlbumId: $linkedRemoteAlbumId, }'''; } } diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index e4e316b814..efccc9bccd 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -67,13 +67,19 @@ enum StoreKey { loadOriginalVideo._(136), manageLocalMediaAndroid._(137), + // Read-only Mode settings + readonlyModeEnabled._(138), + // Experimental stuff photoManagerCustomFilter._(1000), betaPromptShown._(1001), betaTimeline._(1002), enableBackup._(1003), useWifiForUploadVideos._(1004), - useWifiForUploadPhotos._(1005); + useWifiForUploadPhotos._(1005), + needBetaMigration._(1006), + // TODO: Remove this after patching open-api + shouldResetSync._(1007); const StoreKey._(this.id); final int id; diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index b0a66f7d70..380295b4b3 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -1,7 +1,36 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'dart:ui'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +enum AvatarColor { + // do not change this order or reuse indices for other purposes, adding is OK + primary("primary"), + pink("pink"), + red("red"), + yellow("yellow"), + blue("blue"), + green("green"), + purple("purple"), + orange("orange"), + gray("gray"), + amber("amber"); + + final String value; + const AvatarColor(this.value); + + Color toColor({bool isDarkTheme = false}) => switch (this) { + AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), + AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182), + AvatarColor.red => const Color.fromARGB(255, 239, 68, 68), + AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8), + AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246), + AvatarColor.green => const Color.fromARGB(255, 22, 163, 74), + AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234), + AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12), + AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99), + AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6), + }; +} // TODO: Rename to User once Isar is removed class UserDto { @@ -9,7 +38,7 @@ class UserDto { final String email; final String name; final bool isAdmin; - final DateTime updatedAt; + final DateTime? updatedAt; final AvatarColor avatarColor; @@ -31,8 +60,8 @@ class UserDto { required this.id, required this.email, required this.name, - required this.isAdmin, - required this.updatedAt, + this.isAdmin = false, + this.updatedAt, required this.profileChangedAt, this.avatarColor = AvatarColor.primary, this.memoryEnabled = true, @@ -75,6 +104,8 @@ profileChangedAt: $profileChangedAt bool? isPartnerSharedWith, bool? hasProfileImage, DateTime? profileChangedAt, + int? quotaSizeInBytes, + int? quotaUsageInBytes, }) => UserDto( id: id ?? this.id, email: email ?? this.email, @@ -88,6 +119,8 @@ profileChangedAt: $profileChangedAt isPartnerSharedWith: isPartnerSharedWith ?? this.isPartnerSharedWith, hasProfileImage: hasProfileImage ?? this.hasProfileImage, profileChangedAt: profileChangedAt ?? this.profileChangedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, ); @override @@ -95,7 +128,8 @@ profileChangedAt: $profileChangedAt if (identical(this, other)) return true; return other.id == id && - other.updatedAt.isAtSameMomentAs(updatedAt) && + ((updatedAt == null && other.updatedAt == null) || + (updatedAt != null && other.updatedAt != null && other.updatedAt!.isAtSameMomentAs(updatedAt!))) && other.avatarColor == avatarColor && other.email == email && other.name == name && @@ -105,7 +139,9 @@ profileChangedAt: $profileChangedAt other.memoryEnabled == memoryEnabled && other.inTimeline == inTimeline && other.hasProfileImage == hasProfileImage && - other.profileChangedAt.isAtSameMomentAs(profileChangedAt); + other.profileChangedAt.isAtSameMomentAs(profileChangedAt) && + other.quotaSizeInBytes == quotaSizeInBytes && + other.quotaUsageInBytes == quotaUsageInBytes; } @override @@ -121,7 +157,9 @@ profileChangedAt: $profileChangedAt isPartnerSharedBy.hashCode ^ isPartnerSharedWith.hashCode ^ hasProfileImage.hashCode ^ - profileChangedAt.hashCode; + profileChangedAt.hashCode ^ + quotaSizeInBytes.hashCode ^ + quotaUsageInBytes.hashCode; } class PartnerUserDto { diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index c477be1a41..af404051a7 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -1,4 +1,4 @@ -import 'dart:ui'; +import 'package:immich_mobile/domain/models/user.model.dart'; enum UserMetadataKey { // do not change this order! @@ -7,36 +7,6 @@ enum UserMetadataKey { license, } -enum AvatarColor { - // do not change this order or reuse indices for other purposes, adding is OK - primary("primary"), - pink("pink"), - red("red"), - yellow("yellow"), - blue("blue"), - green("green"), - purple("purple"), - orange("orange"), - gray("gray"), - amber("amber"); - - final String value; - const AvatarColor(this.value); - - Color toColor({bool isDarkTheme = false}) => switch (this) { - AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), - AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182), - AvatarColor.red => const Color.fromARGB(255, 239, 68, 68), - AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8), - AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246), - AvatarColor.green => const Color.fromARGB(255, 22, 163, 74), - AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234), - AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12), - AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99), - AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6), - }; -} - class Onboarding { final bool isOnboarded; diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index df34a41e54..7f8ade313c 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,21 +1,20 @@ +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:platform/platform.dart'; class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; - final Platform _platform; const AssetService({ required RemoteAssetRepository remoteAssetRepository, required DriftLocalAssetRepository localAssetRepository, }) : _remoteAssetRepository = remoteAssetRepository, - _localAssetRepository = localAssetRepository, - _platform = const LocalPlatform(); + _localAssetRepository = localAssetRepository; Future getAsset(BaseAsset asset) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; @@ -27,19 +26,26 @@ class AssetService { return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id); } + Future> getLocalAssetsByChecksum(String checksum) { + return _localAssetRepository.getByChecksum(checksum); + } + + Future getRemoteAssetByChecksum(String checksum) { + return _remoteAssetRepository.getByChecksum(checksum); + } + Future getRemoteAsset(String id) { return _remoteAssetRepository.get(id); } Future> getStack(RemoteAsset asset) async { if (asset.stackId == null) { - return []; + return const []; } - return _remoteAssetRepository.getStackChildren(asset).then((assets) { - // Include the primary asset in the stack as the first item - return [asset, ...assets]; - }); + final stack = await _remoteAssetRepository.getStackChildren(asset); + // Include the primary asset in the stack as the first item + return [asset, ...stack]; } Future getExif(BaseAsset asset) async { @@ -62,7 +68,7 @@ class AssetService { width = exif?.width ?? asset.width?.toDouble(); height = exif?.height ?? asset.height?.toDouble(); } else if (asset is LocalAsset) { - isFlipped = _platform.isAndroid && (asset.orientation == 90 || asset.orientation == 270); + isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270); width = asset.width?.toDouble(); height = asset.height?.toDouble(); } else { @@ -78,8 +84,8 @@ class AssetService { return 1.0; } - Future> getPlaces() { - return _remoteAssetRepository.getPlaces(); + Future> getPlaces(String userId) { + return _remoteAssetRepository.getPlaces(userId); } Future<(int local, int remote)> getAssetCounts() async { @@ -89,4 +95,8 @@ class AssetService { Future getLocalHashedCount() { return _localAssetRepository.getHashedCount(); } + + Future> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) { + return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection); + } } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart new file mode 100644 index 0000000000..0548a45bf7 --- /dev/null +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -0,0 +1,309 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:cancellation_token_http/http.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/network_capability_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; +import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/db.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/user.provider.dart'; +import 'package:immich_mobile/repositories/file_media.repository.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/auth.service.dart'; +import 'package:immich_mobile/services/localization.service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:immich_mobile/utils/http_ssl_options.dart'; +import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; +import 'package:worker_manager/worker_manager.dart'; + +class BackgroundWorkerFgService { + final BackgroundWorkerFgHostApi _foregroundHostApi; + + const BackgroundWorkerFgService(this._foregroundHostApi); + + // TODO: Move this call to native side once old timeline is removed + Future enable() => _foregroundHostApi.enable(); + + Future configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure( + BackgroundWorkerSettings( + minimumDelaySeconds: + minimumDelaySeconds ?? + Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue), + requiresCharging: + requireCharging ?? + Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue), + ), + ); + + Future disable() => _foregroundHostApi.disable(); +} + +class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { + ProviderContainer? _ref; + final Isar _isar; + final Drift _drift; + final DriftLogger _driftLogger; + final BackgroundWorkerBgHostApi _backgroundHostApi; + final CancellationToken _cancellationToken = CancellationToken(); + final Logger _logger = Logger('BackgroundWorkerBgService'); + + bool _isCleanedUp = false; + + BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger}) + : _isar = isar, + _drift = drift, + _driftLogger = driftLogger, + _backgroundHostApi = BackgroundWorkerBgHostApi() { + _ref = ProviderContainer( + overrides: [ + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + driftProvider.overrideWith(driftOverride(drift)), + ], + ); + BackgroundWorkerFlutterApi.setUp(this); + } + + bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false; + + Future init() async { + try { + HttpSSLOptions.apply(applyNative: false); + + await Future.wait( + [ + loadTranslations(), + workerManager.init(dynamicSpawning: true), + _ref?.read(authServiceProvider).setOpenApiServiceEndpoint(), + // Initialize the file downloader + FileDownloader().configure( + globalConfig: [ + // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 + (Config.holdingQueue, (6, 6, 3)), + // On Android, if files are larger than 256MB, run in foreground service + (Config.runInForegroundIfFileLargerThan, 256), + ], + ), + FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false), + FileDownloader().trackTasks(), + _ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(), + ].nonNulls, + ); + + configureFileDownloaderNotifications(); + + if (Platform.isAndroid) { + await _backgroundHostApi.showNotification( + IntlKeys.uploading_media.t(), + IntlKeys.backup_background_service_default_notification.t(), + ); + } + + // Notify the host that the background worker service has been initialized and is ready to use + _backgroundHostApi.onInitialized(); + } catch (error, stack) { + _logger.severe("Failed to initialize background worker", error, stack); + _backgroundHostApi.close(); + } + } + + @override + Future onAndroidUpload() async { + _logger.info('Android background processing started'); + final sw = Stopwatch()..start(); + try { + if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) { + _logger.warning("Remote sync did not complete successfully, skipping backup"); + return; + } + await _handleBackup(); + } catch (error, stack) { + _logger.severe("Failed to complete Android background processing", error, stack); + } finally { + sw.stop(); + _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); + await _cleanup(); + } + } + + @override + Future onIosUpload(bool isRefresh, int? maxSeconds) async { + _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); + final sw = Stopwatch()..start(); + try { + final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); + if (!await _syncAssets(hashTimeout: timeout)) { + _logger.warning("Remote sync did not complete successfully, skipping backup"); + return; + } + + final backupFuture = _handleBackup(); + if (maxSeconds != null) { + await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); + } else { + await backupFuture; + } + } catch (error, stack) { + _logger.severe("Failed to complete iOS background upload", error, stack); + } finally { + sw.stop(); + _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); + await _cleanup(); + } + } + + @override + Future cancel() async { + _logger.warning("Background worker cancelled"); + try { + await _cleanup(); + } catch (error, stack) { + dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); + } + } + + Future _cleanup() async { + // If ref is null, it means the service was never initialized properly + if (_isCleanedUp || _ref == null) { + return; + } + + try { + _isCleanedUp = true; + final backgroundSyncManager = _ref?.read(backgroundSyncProvider); + final nativeSyncApi = _ref?.read(nativeSyncApiProvider); + _ref?.dispose(); + _ref = null; + + _cancellationToken.cancel(); + _logger.info("Cleaning up background worker"); + final cleanupFutures = [ + workerManager.dispose().catchError((_) async { + // Discard any errors on the dispose call + return; + }), + LogService.I.dispose(), + Store.dispose(), + _drift.close(), + _driftLogger.close(), + backgroundSyncManager?.cancel(), + nativeSyncApi?.cancelHashing(), + ]; + + if (_isar.isOpen) { + cleanupFutures.add(_isar.close()); + } + await Future.wait(cleanupFutures.nonNulls); + _logger.info("Background worker resources cleaned up"); + } catch (error, stack) { + dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); + } + } + + Future _handleBackup() async { + await runZonedGuarded( + () async { + if (_isCleanedUp) { + return; + } + + if (!_isBackupEnabled) { + _logger.info("Backup is disabled. Skipping backup routine"); + return; + } + + final currentUser = _ref?.read(currentUserProvider); + if (currentUser == null) { + _logger.warning("No current user found. Skipping backup from background"); + return; + } + + if (Platform.isIOS) { + return _ref?.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); + } + + final networkCapabilities = await _ref?.read(connectivityApiProvider).getCapabilities() ?? []; + return _ref + ?.read(uploadServiceProvider) + .startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken); + }, + (error, stack) { + dPrint(() => "Error in backup zone $error, $stack"); + }, + ); + } + + Future _syncAssets({Duration? hashTimeout}) async { + await _ref?.read(backgroundSyncProvider).syncLocal(); + if (_isCleanedUp) { + return false; + } + + final isSuccess = await _ref?.read(backgroundSyncProvider).syncRemote() ?? false; + if (_isCleanedUp) { + return isSuccess; + } + + var hashFuture = _ref?.read(backgroundSyncProvider).hashAssets(); + if (hashTimeout != null && hashFuture != null) { + hashFuture = hashFuture.timeout( + hashTimeout, + onTimeout: () { + // Consume cancellation errors as we want to continue processing + }, + ); + } + + await hashFuture; + return isSuccess; + } +} + +class BackgroundWorkerLockService { + final BackgroundWorkerLockApi _hostApi; + const BackgroundWorkerLockService(this._hostApi); + + Future lock() async { + if (CurrentPlatform.isAndroid) { + return _hostApi.lock(); + } + } + + Future unlock() async { + if (CurrentPlatform.isAndroid) { + return _hostApi.unlock(); + } + } +} + +/// Native entry invoked from the background worker. If renaming or moving this to a different +/// library, make sure to update the entry points and URI in native workers as well +@pragma('vm:entry-point') +Future backgroundSyncNativeEntrypoint() async { + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + + final (isar, drift, logDB) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false); + await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); +} diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index a8eea2c25e..90f29b8bc1 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -1,46 +1,61 @@ -import 'dart:convert'; - +import 'package:flutter/services.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:logging/logging.dart'; +const String _kHashCancelledCode = "HASH_CANCELLED"; + class HashService { - final int batchSizeLimit; - final int batchFileLimit; + final int _batchSize; final DriftLocalAlbumRepository _localAlbumRepository; final DriftLocalAssetRepository _localAssetRepository; - final StorageRepository _storageRepository; final NativeSyncApi _nativeSyncApi; + final bool Function()? _cancelChecker; final _log = Logger('HashService'); HashService({ required DriftLocalAlbumRepository localAlbumRepository, required DriftLocalAssetRepository localAssetRepository, - required StorageRepository storageRepository, required NativeSyncApi nativeSyncApi, - this.batchSizeLimit = kBatchHashSizeLimit, - this.batchFileLimit = kBatchHashFileLimit, + bool Function()? cancelChecker, + int? batchSize, }) : _localAlbumRepository = localAlbumRepository, _localAssetRepository = localAssetRepository, - _storageRepository = storageRepository, - _nativeSyncApi = nativeSyncApi; + _cancelChecker = cancelChecker, + _nativeSyncApi = nativeSyncApi, + _batchSize = batchSize ?? kBatchHashFileLimit; + + bool get isCancelled => _cancelChecker?.call() ?? false; Future hashAssets() async { + _log.info("Starting hashing of assets"); final Stopwatch stopwatch = Stopwatch()..start(); - // Sorted by backupSelection followed by isCloud - final localAlbums = await _localAlbumRepository.getAll( - sortBy: {SortLocalAlbumsBy.backupSelection, SortLocalAlbumsBy.isIosSharedAlbum}, - ); + try { + // Sorted by backupSelection followed by isCloud + final localAlbums = await _localAlbumRepository.getBackupAlbums(); - for (final album in localAlbums) { - final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id); - if (assetsToHash.isNotEmpty) { - await _hashAssets(assetsToHash); + for (final album in localAlbums) { + if (isCancelled) { + _log.warning("Hashing cancelled. Stopped processing albums."); + break; + } + + final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id); + if (assetsToHash.isNotEmpty) { + await _hashAssets(album, assetsToHash); + } } + } on PlatformException catch (e) { + if (e.code == _kHashCancelledCode) { + _log.warning("Hashing cancelled by platform"); + return; + } + } catch (e, s) { + _log.severe("Error during hashing", e, s); } stopwatch.stop(); @@ -50,64 +65,62 @@ class HashService { /// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB /// with hash for those that were successfully hashed. Hashes are looked up in a table /// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB. - Future _hashAssets(List assetsToHash) async { - int bytesProcessed = 0; - final toHash = <_AssetToPath>[]; + Future _hashAssets(LocalAlbum album, List assetsToHash) async { + final toHash = {}; for (final asset in assetsToHash) { - final file = await _storageRepository.getFileForAsset(asset.id); - if (file == null) { - continue; + if (isCancelled) { + _log.warning("Hashing cancelled. Stopped processing assets."); + return; } - bytesProcessed += await file.length(); - toHash.add(_AssetToPath(asset: asset, path: file.path)); - - if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) { - await _processBatch(toHash); + toHash[asset.id] = asset; + if (toHash.length == _batchSize) { + await _processBatch(album, toHash); toHash.clear(); - bytesProcessed = 0; } } - await _processBatch(toHash); + await _processBatch(album, toHash); } /// Processes a batch of assets. - Future _processBatch(List<_AssetToPath> toHash) async { + Future _processBatch(LocalAlbum album, Map toHash) async { if (toHash.isEmpty) { return; } _log.fine("Hashing ${toHash.length} files"); - final hashed = []; - final hashes = await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList()); + final hashed = {}; + final hashResults = await _nativeSyncApi.hashAssets( + toHash.keys.toList(), + allowNetworkAccess: album.backupSelection == BackupSelection.selected, + ); assert( - hashes.length == toHash.length, - "Hashes length does not match toHash length: ${hashes.length} != ${toHash.length}", + hashResults.length == toHash.length, + "Hashes length does not match toHash length: ${hashResults.length} != ${toHash.length}", ); - for (int i = 0; i < hashes.length; i++) { - final hash = hashes[i]; - final asset = toHash[i].asset; - if (hash?.length == 20) { - hashed.add(asset.copyWith(checksum: base64.encode(hash!))); + for (int i = 0; i < hashResults.length; i++) { + if (isCancelled) { + _log.warning("Hashing cancelled. Stopped processing batch."); + return; + } + + final hashResult = hashResults[i]; + if (hashResult.hash != null) { + hashed[hashResult.assetId] = hashResult.hash!; } else { - _log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}"); + final asset = toHash[hashResult.assetId]; + _log.warning( + "Failed to hash asset with id: ${hashResult.assetId}, name: ${asset?.name}, createdAt: ${asset?.createdAt}, from album: ${album.name}. Error: ${hashResult.error ?? "unknown"}", + ); } } _log.fine("Hashed ${hashed.length}/${toHash.length} assets"); await _localAssetRepository.updateHashes(hashed); - await _storageRepository.clearCache(); } } - -class _AssetToPath { - final LocalAsset asset; - final String path; - - const _AssetToPath({required this.asset, required this.path}); -} diff --git a/mobile/lib/domain/services/local_album.service.dart b/mobile/lib/domain/services/local_album.service.dart index 6c1479fdc9..e3d888f063 100644 --- a/mobile/lib/domain/services/local_album.service.dart +++ b/mobile/lib/domain/services/local_album.service.dart @@ -22,4 +22,16 @@ class LocalAlbumService { Future getCount() { return _repository.getCount(); } + + Future unlinkRemoteAlbum(String id) async { + return _repository.unlinkRemoteAlbum(id); + } + + Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async { + return _repository.linkRemoteAlbum(localAlbumId, remoteAlbumId); + } + + Future> getBackupAlbums() { + return _repository.getBackupAlbums(); + } } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 119954cb47..ca356c80d8 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -1,28 +1,24 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; -import 'package:platform/platform.dart'; class LocalSyncService { final DriftLocalAlbumRepository _localAlbumRepository; final NativeSyncApi _nativeSyncApi; - final Platform _platform; final Logger _log = Logger("DeviceSyncService"); - LocalSyncService({ - required DriftLocalAlbumRepository localAlbumRepository, - required NativeSyncApi nativeSyncApi, - Platform? platform, - }) : _localAlbumRepository = localAlbumRepository, - _nativeSyncApi = nativeSyncApi, - _platform = platform ?? const LocalPlatform(); + LocalSyncService({required DriftLocalAlbumRepository localAlbumRepository, required NativeSyncApi nativeSyncApi}) + : _localAlbumRepository = localAlbumRepository, + _nativeSyncApi = nativeSyncApi; Future sync({bool full = false}) async { final Stopwatch stopwatch = Stopwatch()..start(); @@ -52,14 +48,14 @@ class LocalSyncService { 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) { + if (CurrentPlatform.isAndroid) { for (final album in dbAlbums) { final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); await _localAlbumRepository.syncDeletes(album.id, deviceIds); } } - if (_platform.isIOS) { + if (CurrentPlatform.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 @@ -285,7 +281,7 @@ extension on Iterable { (e) => LocalAlbum( id: e.id, name: e.name, - updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(), assetCount: e.assetCount, ), ).toList(); @@ -300,8 +296,8 @@ extension on Iterable { name: e.name, checksum: null, 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), + createdAt: tryFromSecondsSinceEpoch(e.createdAt, isUtc: true) ?? DateTime.timestamp(), + updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(), width: e.width, height: e.height, durationInSeconds: e.durationInSeconds, diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 1053d5e54f..64010b9220 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; /// Service responsible for handling application logging. @@ -66,13 +66,12 @@ class LogService { } void _handleLogRecord(LogRecord r) { - if (kDebugMode) { - debugPrint( - '[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}' - '${r.error == null ? '' : '\nError: ${r.error}'}' - '${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}', - ); - } + dPrint( + () => + '[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}' + '${r.error == null ? '' : '\nError: ${r.error}'}' + '${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}', + ); final record = LogMessage( message: r.message, @@ -123,6 +122,11 @@ class LogService { _flushTimer = null; final buffer = [..._msgBuffer]; _msgBuffer.clear(); + + if (buffer.isEmpty) { + return; + } + await _logRepository.insertAll(buffer); } } diff --git a/mobile/lib/domain/services/partner.service.dart b/mobile/lib/domain/services/partner.service.dart index 7733b5be6b..ce1bd9557b 100644 --- a/mobile/lib/domain/services/partner.service.dart +++ b/mobile/lib/domain/services/partner.service.dart @@ -1,7 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class DriftPartnerService { final DriftPartnerRepository _driftPartnerRepository; @@ -30,7 +30,7 @@ class DriftPartnerService { Future toggleShowInTimeline(String partnerId, String userId) async { final partner = await _driftPartnerRepository.getPartner(partnerId, userId); if (partner == null) { - debugPrint("Partner not found: $partnerId for user: $userId"); + dPrint(() => "Partner not found: $partnerId for user: $userId"); return; } diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 4d85119b74..cc28dfafd5 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -26,6 +26,10 @@ class RemoteAlbumService { return _repository.get(albumId); } + Future getByName(String albumName, String ownerId) { + return _repository.getByName(albumName, ownerId); + } + Future> sortAlbums( List albums, RemoteAlbumSortMode sortMode, { @@ -80,7 +84,6 @@ class RemoteAlbumService { Future createAlbum({required String title, required List assetIds, String? description}) async { final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds); - await _repository.create(album, assetIds); return album; diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 3347134ae6..f9b4a0aa81 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -10,7 +10,7 @@ class StoreService { /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; - late final StreamSubscription> _storeUpdateSubscription; + StreamSubscription>? _storeUpdateSubscription; StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; @@ -24,15 +24,17 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IStoreRepository storeRepository}) async { - _instance ??= await create(storeRepository: storeRepository); + static Future init({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); return _instance!; } - static Future create({required IStoreRepository storeRepository}) async { + static Future create({required IStoreRepository storeRepository, bool listenUpdates = true}) async { final instance = StoreService._(isarStoreRepository: storeRepository); await instance.populateCache(); - instance._storeUpdateSubscription = instance._listenForChange(); + if (listenUpdates) { + instance._storeUpdateSubscription = instance._listenForChange(); + } return instance; } @@ -50,8 +52,8 @@ class StoreService { }); /// Disposes the store and cancels the subscription. To reuse the store call init() again - void dispose() async { - await _storeUpdateSubscription.cancel(); + Future dispose() async { + await _storeUpdateSubscription?.cancel(); _cache.clear(); } @@ -90,7 +92,7 @@ class StoreService { _cache.clear(); } - bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false; + bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/sync_linked_album.service.dart b/mobile/lib/domain/services/sync_linked_album.service.dart new file mode 100644 index 0000000000..b61ca1c965 --- /dev/null +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -0,0 +1,110 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:logging/logging.dart'; + +final syncLinkedAlbumServiceProvider = Provider( + (ref) => SyncLinkedAlbumService( + ref.watch(localAlbumRepository), + ref.watch(remoteAlbumRepository), + ref.watch(driftAlbumApiRepositoryProvider), + ), +); + +class SyncLinkedAlbumService { + final DriftLocalAlbumRepository _localAlbumRepository; + final DriftRemoteAlbumRepository _remoteAlbumRepository; + final DriftAlbumApiRepository _albumApiRepository; + + SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository); + + final _log = Logger("SyncLinkedAlbumService"); + + Future syncLinkedAlbums(String userId) async { + final selectedAlbums = await _localAlbumRepository.getBackupAlbums(); + + await Future.wait( + selectedAlbums.map((localAlbum) async { + final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId; + if (linkedRemoteAlbumId == null) { + _log.warning("No linked remote album ID found for local album: ${localAlbum.name}"); + return; + } + + final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId); + if (remoteAlbum == null) { + _log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId"); + return; + } + + // get assets that are uploaded but not in the remote album + final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId); + _log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}"); + if (assetIds.isNotEmpty) { + final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds); + await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added); + } + }), + ); + } + + Future manageLinkedAlbums(List localAlbums, String ownerId) async { + try { + for (final album in localAlbums) { + await _processLocalAlbum(album, ownerId); + } + } catch (error, stackTrace) { + _log.severe("Error managing linked albums", error, stackTrace); + } + } + + /// Processes a single local album to ensure proper linking with remote albums + Future _processLocalAlbum(LocalAlbum localAlbum, String ownerId) { + final hasLinkedRemoteAlbum = localAlbum.linkedRemoteAlbumId != null; + + if (hasLinkedRemoteAlbum) { + return _handleLinkedAlbum(localAlbum); + } else { + return _handleUnlinkedAlbum(localAlbum, ownerId); + } + } + + /// Handles albums that are already linked to a remote album + Future _handleLinkedAlbum(LocalAlbum localAlbum) async { + final remoteAlbumId = localAlbum.linkedRemoteAlbumId!; + final remoteAlbum = await _remoteAlbumRepository.get(remoteAlbumId); + + final remoteAlbumExists = remoteAlbum != null; + if (!remoteAlbumExists) { + return _localAlbumRepository.unlinkRemoteAlbum(localAlbum.id); + } + } + + /// Handles albums that are not linked to any remote album + Future _handleUnlinkedAlbum(LocalAlbum localAlbum, String ownerId) async { + final existingRemoteAlbum = await _remoteAlbumRepository.getByName(localAlbum.name, ownerId); + + if (existingRemoteAlbum != null) { + return _linkToExistingRemoteAlbum(localAlbum, existingRemoteAlbum); + } else { + return _createAndLinkNewRemoteAlbum(localAlbum); + } + } + + /// Links a local album to an existing remote album + Future _linkToExistingRemoteAlbum(LocalAlbum localAlbum, dynamic existingRemoteAlbum) { + return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, existingRemoteAlbum.id); + } + + /// Creates a new remote album and links it to the local album + Future _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async { + dPrint(() => "Creating new remote album for local album: ${localAlbum.name}"); + final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []); + await _remoteAlbumRepository.create(newRemoteAlbum, []); + return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id); + } +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 5625635e49..bec7e6afda 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -23,54 +23,19 @@ class SyncStreamService { bool get isCancelled => _cancelChecker?.call() ?? false; - Future sync() { + Future sync() async { _logger.info("Remote sync request for user"); // Start the sync stream and handle events - return _syncApiRepository.streamChanges(_handleEvents); - } - - Future handleWsAssetUploadReadyV1Batch(List batchData) async { - if (batchData.isEmpty) return; - - _logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events'); - - final List assets = []; - final List exifs = []; - - try { - for (final data in batchData) { - if (data is! Map) { - continue; - } - - final payload = data; - final assetData = payload['asset']; - final exifData = payload['exif']; - - if (assetData == null || exifData == null) { - continue; - } - - final asset = SyncAssetV1.fromJson(assetData); - final exif = SyncAssetExifV1.fromJson(exifData); - - if (asset != null && exif != null) { - assets.add(asset); - exifs.add(exif); - } - } - - if (assets.isNotEmpty && exifs.isNotEmpty) { - await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch'); - await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch'); - _logger.info('Successfully processed ${assets.length} assets in batch'); - } - } catch (error, stackTrace) { - _logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace); + bool shouldReset = false; + await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true); + if (shouldReset) { + _logger.info("Resetting sync state as requested by server"); + await _syncApiRepository.streamChanges(_handleEvents); } + return true; } - Future _handleEvents(List events, Function() abort) async { + Future _handleEvents(List events, Function() abort, Function() reset) async { List items = []; for (final event in events) { if (isCancelled) { @@ -83,6 +48,10 @@ class SyncStreamService { await _processBatch(items); } + if (event.type == SyncEntityType.syncResetV1) { + reset(); + } + items.add(event); } @@ -103,6 +72,8 @@ class SyncStreamService { Future _handleSyncData(SyncEntityType type, Iterable data) async { _logger.fine("Processing sync data for $type of length ${data.length}"); switch (type) { + case SyncEntityType.authUserV1: + return _syncStreamRepository.updateAuthUsersV1(data.cast()); case SyncEntityType.userV1: return _syncStreamRepository.updateUsersV1(data.cast()); case SyncEntityType.userDeleteV1: @@ -159,6 +130,12 @@ class SyncStreamService { // to acknowledge that the client has processed all the backfill events case SyncEntityType.syncAckV1: return; + // No-op. SyncCompleteV1 is used to signal the completion of the sync process + case SyncEntityType.syncCompleteV1: + return; + // Request to reset the client state. Clear everything related to remote entities + case SyncEntityType.syncResetV1: + return _syncStreamRepository.reset(); case SyncEntityType.memoryV1: return _syncStreamRepository.updateMemoriesV1(data.cast()); case SyncEntityType.memoryDeleteV1: @@ -193,4 +170,45 @@ class SyncStreamService { _logger.warning("Unknown sync data type: $type"); } } + + Future handleWsAssetUploadReadyV1Batch(List batchData) async { + if (batchData.isEmpty) return; + + _logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events'); + + final List assets = []; + final List exifs = []; + + try { + for (final data in batchData) { + if (data is! Map) { + continue; + } + + final payload = data; + final assetData = payload['asset']; + final exifData = payload['exif']; + + if (assetData == null || exifData == null) { + continue; + } + + final asset = SyncAssetV1.fromJson(assetData); + final exif = SyncAssetExifV1.fromJson(exifData); + + if (asset != null && exif != null) { + assets.add(asset); + exifs.add(exif); + } + } + + if (assets.isNotEmpty && exifs.isNotEmpty) { + await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch'); + await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch'); + _logger.info('Successfully processed ${assets.length} assets in batch'); + } + } catch (error, stackTrace) { + _logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace); + } + } } diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index ab623720b7..bf354847c3 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -59,7 +59,8 @@ class TimelineFactory { TimelineService fromAssets(List assets) => TimelineService(_timelineRepository.fromAssets(assets)); - TimelineService map(LatLngBounds bounds) => TimelineService(_timelineRepository.map(bounds, groupBy)); + TimelineService map(String userId, LatLngBounds bounds) => + TimelineService(_timelineRepository.map(userId, bounds, groupBy)); } class TimelineService { diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index cbf4030788..b2f2fe54e1 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:immich_mobile/domain/utils/sync_linked_album.dart'; import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -20,9 +21,10 @@ class BackgroundSyncManager { final SyncCallback? onHashingComplete; final SyncErrorCallback? onHashingError; - Cancelable? _syncTask; + Cancelable? _syncTask; Cancelable? _syncWebsocketTask; Cancelable? _deviceAlbumSyncTask; + Cancelable? _linkedAlbumSyncTask; Cancelable? _hashTask; BackgroundSyncManager({ @@ -52,6 +54,34 @@ class BackgroundSyncManager { _syncWebsocketTask?.cancel(); _syncWebsocketTask = null; + if (_linkedAlbumSyncTask != null) { + futures.add(_linkedAlbumSyncTask!.future); + } + _linkedAlbumSyncTask?.cancel(); + _linkedAlbumSyncTask = null; + + try { + await Future.wait(futures); + } on CanceledError { + // Ignore cancellation errors + } + } + + Future cancelLocal() async { + final futures = []; + + if (_hashTask != null) { + futures.add(_hashTask!.future); + } + _hashTask?.cancel(); + _hashTask = null; + + if (_deviceAlbumSyncTask != null) { + futures.add(_deviceAlbumSyncTask!.future); + } + _deviceAlbumSyncTask?.cancel(); + _deviceAlbumSyncTask = null; + try { await Future.wait(futures); } on CanceledError { @@ -70,8 +100,14 @@ class BackgroundSyncManager { // 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)); + ? runInIsolateGentle( + computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true), + debugLabel: 'local-sync-full-true', + ) + : runInIsolateGentle( + computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false), + debugLabel: 'local-sync-full-false', + ); return _deviceAlbumSyncTask! .whenComplete(() { @@ -92,7 +128,10 @@ class BackgroundSyncManager { onHashingStart?.call(); - _hashTask = runInIsolateGentle(computation: (ref) => ref.read(hashServiceProvider).hashAssets()); + _hashTask = runInIsolateGentle( + computation: (ref) => ref.read(hashServiceProvider).hashAssets(), + debugLabel: 'hash-assets', + ); return _hashTask! .whenComplete(() { @@ -105,15 +144,19 @@ class BackgroundSyncManager { }); } - Future syncRemote() { + Future syncRemote() { if (_syncTask != null) { - return _syncTask!.future; + return _syncTask!.future.then((result) => result ?? false).catchError((_) => false); } onRemoteSyncStart?.call(); - _syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync()); + _syncTask = runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).sync(), + debugLabel: 'remote-sync', + ); return _syncTask! + .then((result) => result ?? false) .whenComplete(() { onRemoteSyncComplete?.call(); _syncTask = null; @@ -121,6 +164,7 @@ class BackgroundSyncManager { .catchError((error) { onRemoteSyncError?.call(error.toString()); _syncTask = null; + return false; }); } @@ -133,8 +177,20 @@ class BackgroundSyncManager { _syncWebsocketTask = null; }); } + + Future syncLinkedAlbum() { + if (_linkedAlbumSyncTask != null) { + return _linkedAlbumSyncTask!.future; + } + + _linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync'); + return _linkedAlbumSyncTask!.whenComplete(() { + _linkedAlbumSyncTask = null; + }); + } } Cancelable _handleWsAssetUploadReadyV1Batch(List batchData) => runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData), + debugLabel: 'websocket-batch', ); diff --git a/mobile/lib/domain/utils/sync_linked_album.dart b/mobile/lib/domain/utils/sync_linked_album.dart new file mode 100644 index 0000000000..7bfadc96e7 --- /dev/null +++ b/mobile/lib/domain/utils/sync_linked_album.dart @@ -0,0 +1,14 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:logging/logging.dart'; + +Future syncLinkedAlbumsIsolated(ProviderContainer ref) { + final user = Store.tryGet(StoreKey.currentUser); + if (user == null) { + Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync"); + return Future.value(); + } + return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id); +} diff --git a/mobile/lib/extensions/network_capability_extensions.dart b/mobile/lib/extensions/network_capability_extensions.dart new file mode 100644 index 0000000000..aeefc11e39 --- /dev/null +++ b/mobile/lib/extensions/network_capability_extensions.dart @@ -0,0 +1,8 @@ +import 'package:immich_mobile/platform/connectivity_api.g.dart'; + +extension NetworkCapabilitiesGetters on List { + bool get hasCellular => contains(NetworkCapability.cellular); + bool get hasWifi => contains(NetworkCapability.wifi); + bool get hasVpn => contains(NetworkCapability.vpn); + bool get isUnmetered => contains(NetworkCapability.unmetered); +} diff --git a/mobile/lib/extensions/platform_extensions.dart b/mobile/lib/extensions/platform_extensions.dart new file mode 100644 index 0000000000..7353fbc6f6 --- /dev/null +++ b/mobile/lib/extensions/platform_extensions.dart @@ -0,0 +1,9 @@ +import 'package:flutter/foundation.dart'; + +extension CurrentPlatform on TargetPlatform { + @pragma('vm:prefer-inline') + static bool get isIOS => defaultTargetPlatform == TargetPlatform.iOS; + + @pragma('vm:prefer-inline') + static bool get isAndroid => defaultTargetPlatform == TargetPlatform.android; +} diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 6cd6e1e4b4..ae31565044 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + extension StringExtension on String { String capitalize() { return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" "); @@ -23,3 +25,11 @@ extension DurationExtension on String { return int.parse(this); } } + +Map? tryJsonDecode(dynamic json) { + try { + return jsonDecode(json) as Map; + } catch (e) { + return null; + } +} diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index cfd8c8cd1f..7677f3cbd8 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:intl/message_format.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { @@ -39,7 +40,7 @@ String _translateHelper(BuildContext? context, String key, [Map? ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args) : translatedMessage; } catch (e) { - debugPrint('Translation failed for key "$key". Error: $e'); + dPrint(() => 'Translation failed for key "$key". Error: $e'); return key; } } diff --git a/mobile/lib/infrastructure/entities/auth_user.entity.dart b/mobile/lib/infrastructure/entities/auth_user.entity.dart new file mode 100644 index 0000000000..cbbeaf536f --- /dev/null +++ b/mobile/lib/infrastructure/entities/auth_user.entity.dart @@ -0,0 +1,27 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class AuthUserEntity extends Table with DriftDefaultsMixin { + const AuthUserEntity(); + + TextColumn get id => text()(); + TextColumn get name => text()(); + TextColumn get email => text()(); + BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); + + // Profile image + BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))(); + DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get avatarColor => intEnum()(); + + // Quota + IntColumn get quotaSizeInBytes => integer().withDefault(const Constant(0))(); + IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))(); + + // Locked Folder + TextColumn get pinCode => text().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/auth_user.entity.drift.dart b/mobile/lib/infrastructure/entities/auth_user.entity.drift.dart new file mode 100644 index 0000000000..4dba1c42fb --- /dev/null +++ b/mobile/lib/infrastructure/entities/auth_user.entity.drift.dart @@ -0,0 +1,933 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/user.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$AuthUserEntityTableCreateCompanionBuilder = + i1.AuthUserEntityCompanion Function({ + required String id, + required String name, + required String email, + i0.Value isAdmin, + i0.Value hasProfileImage, + i0.Value profileChangedAt, + required i2.AvatarColor avatarColor, + i0.Value quotaSizeInBytes, + i0.Value quotaUsageInBytes, + i0.Value pinCode, + }); +typedef $$AuthUserEntityTableUpdateCompanionBuilder = + i1.AuthUserEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value email, + i0.Value isAdmin, + i0.Value hasProfileImage, + i0.Value profileChangedAt, + i0.Value avatarColor, + i0.Value quotaSizeInBytes, + i0.Value quotaUsageInBytes, + i0.Value pinCode, + }); + +class $$AuthUserEntityTableFilterComposer + extends i0.Composer { + $$AuthUserEntityTableFilterComposer({ + 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 email => $composableBuilder( + column: $table.email, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get isAdmin => $composableBuilder( + column: $table.isAdmin, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get hasProfileImage => $composableBuilder( + column: $table.hasProfileImage, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get profileChangedAt => $composableBuilder( + column: $table.profileChangedAt, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnWithTypeConverterFilters + get avatarColor => $composableBuilder( + column: $table.avatarColor, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); + + i0.ColumnFilters get quotaSizeInBytes => $composableBuilder( + column: $table.quotaSizeInBytes, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get quotaUsageInBytes => $composableBuilder( + column: $table.quotaUsageInBytes, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get pinCode => $composableBuilder( + column: $table.pinCode, + builder: (column) => i0.ColumnFilters(column), + ); +} + +class $$AuthUserEntityTableOrderingComposer + extends i0.Composer { + $$AuthUserEntityTableOrderingComposer({ + 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 email => $composableBuilder( + column: $table.email, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get isAdmin => $composableBuilder( + column: $table.isAdmin, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get hasProfileImage => $composableBuilder( + column: $table.hasProfileImage, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get profileChangedAt => $composableBuilder( + column: $table.profileChangedAt, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get avatarColor => $composableBuilder( + column: $table.avatarColor, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get quotaSizeInBytes => $composableBuilder( + column: $table.quotaSizeInBytes, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get quotaUsageInBytes => $composableBuilder( + column: $table.quotaUsageInBytes, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get pinCode => $composableBuilder( + column: $table.pinCode, + builder: (column) => i0.ColumnOrderings(column), + ); +} + +class $$AuthUserEntityTableAnnotationComposer + extends i0.Composer { + $$AuthUserEntityTableAnnotationComposer({ + 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 email => + $composableBuilder(column: $table.email, builder: (column) => column); + + i0.GeneratedColumn get isAdmin => + $composableBuilder(column: $table.isAdmin, builder: (column) => column); + + i0.GeneratedColumn get hasProfileImage => $composableBuilder( + column: $table.hasProfileImage, + builder: (column) => column, + ); + + i0.GeneratedColumn get profileChangedAt => $composableBuilder( + column: $table.profileChangedAt, + builder: (column) => column, + ); + + i0.GeneratedColumnWithTypeConverter get avatarColor => + $composableBuilder( + column: $table.avatarColor, + builder: (column) => column, + ); + + i0.GeneratedColumn get quotaSizeInBytes => $composableBuilder( + column: $table.quotaSizeInBytes, + builder: (column) => column, + ); + + i0.GeneratedColumn get quotaUsageInBytes => $composableBuilder( + column: $table.quotaUsageInBytes, + builder: (column) => column, + ); + + i0.GeneratedColumn get pinCode => + $composableBuilder(column: $table.pinCode, builder: (column) => column); +} + +class $$AuthUserEntityTableTableManager + extends + i0.RootTableManager< + i0.GeneratedDatabase, + i1.$AuthUserEntityTable, + i1.AuthUserEntityData, + i1.$$AuthUserEntityTableFilterComposer, + i1.$$AuthUserEntityTableOrderingComposer, + i1.$$AuthUserEntityTableAnnotationComposer, + $$AuthUserEntityTableCreateCompanionBuilder, + $$AuthUserEntityTableUpdateCompanionBuilder, + ( + i1.AuthUserEntityData, + i0.BaseReferences< + i0.GeneratedDatabase, + i1.$AuthUserEntityTable, + i1.AuthUserEntityData + >, + ), + i1.AuthUserEntityData, + i0.PrefetchHooks Function() + > { + $$AuthUserEntityTableTableManager( + i0.GeneratedDatabase db, + i1.$AuthUserEntityTable table, + ) : super( + i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$AuthUserEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$AuthUserEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => i1 + .$$AuthUserEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value email = const i0.Value.absent(), + i0.Value isAdmin = const i0.Value.absent(), + i0.Value hasProfileImage = const i0.Value.absent(), + i0.Value profileChangedAt = const i0.Value.absent(), + i0.Value avatarColor = const i0.Value.absent(), + i0.Value quotaSizeInBytes = const i0.Value.absent(), + i0.Value quotaUsageInBytes = const i0.Value.absent(), + i0.Value pinCode = const i0.Value.absent(), + }) => i1.AuthUserEntityCompanion( + id: id, + name: name, + email: email, + isAdmin: isAdmin, + hasProfileImage: hasProfileImage, + profileChangedAt: profileChangedAt, + avatarColor: avatarColor, + quotaSizeInBytes: quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes, + pinCode: pinCode, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required String email, + i0.Value isAdmin = const i0.Value.absent(), + i0.Value hasProfileImage = const i0.Value.absent(), + i0.Value profileChangedAt = const i0.Value.absent(), + required i2.AvatarColor avatarColor, + i0.Value quotaSizeInBytes = const i0.Value.absent(), + i0.Value quotaUsageInBytes = const i0.Value.absent(), + i0.Value pinCode = const i0.Value.absent(), + }) => i1.AuthUserEntityCompanion.insert( + id: id, + name: name, + email: email, + isAdmin: isAdmin, + hasProfileImage: hasProfileImage, + profileChangedAt: profileChangedAt, + avatarColor: avatarColor, + quotaSizeInBytes: quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes, + pinCode: pinCode, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$AuthUserEntityTableProcessedTableManager = + i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$AuthUserEntityTable, + i1.AuthUserEntityData, + i1.$$AuthUserEntityTableFilterComposer, + i1.$$AuthUserEntityTableOrderingComposer, + i1.$$AuthUserEntityTableAnnotationComposer, + $$AuthUserEntityTableCreateCompanionBuilder, + $$AuthUserEntityTableUpdateCompanionBuilder, + ( + i1.AuthUserEntityData, + i0.BaseReferences< + i0.GeneratedDatabase, + i1.$AuthUserEntityTable, + i1.AuthUserEntityData + >, + ), + i1.AuthUserEntityData, + i0.PrefetchHooks Function() + >; + +class $AuthUserEntityTable extends i3.AuthUserEntity + with i0.TableInfo<$AuthUserEntityTable, i1.AuthUserEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $AuthUserEntityTable(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 _emailMeta = const i0.VerificationMeta( + 'email', + ); + @override + late final i0.GeneratedColumn email = i0.GeneratedColumn( + 'email', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + ); + static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta( + 'isAdmin', + ); + @override + late final i0.GeneratedColumn isAdmin = i0.GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const i4.Constant(false), + ); + static const i0.VerificationMeta _hasProfileImageMeta = + const i0.VerificationMeta('hasProfileImage'); + @override + late final i0.GeneratedColumn hasProfileImage = + i0.GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const i4.Constant(false), + ); + static const i0.VerificationMeta _profileChangedAtMeta = + const i0.VerificationMeta('profileChangedAt'); + @override + late final i0.GeneratedColumn profileChangedAt = + i0.GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime, + ); + @override + late final i0.GeneratedColumnWithTypeConverter + avatarColor = + i0.GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter( + i1.$AuthUserEntityTable.$converteravatarColor, + ); + static const i0.VerificationMeta _quotaSizeInBytesMeta = + const i0.VerificationMeta('quotaSizeInBytes'); + @override + late final i0.GeneratedColumn quotaSizeInBytes = i0.GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0), + ); + static const i0.VerificationMeta _quotaUsageInBytesMeta = + const i0.VerificationMeta('quotaUsageInBytes'); + @override + late final i0.GeneratedColumn quotaUsageInBytes = + i0.GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0), + ); + static const i0.VerificationMeta _pinCodeMeta = const i0.VerificationMeta( + 'pinCode', + ); + @override + late final i0.GeneratedColumn pinCode = i0.GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_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('email')) { + context.handle( + _emailMeta, + email.isAcceptableOrUnknown(data['email']!, _emailMeta), + ); + } else if (isInserting) { + context.missing(_emailMeta); + } + if (data.containsKey('is_admin')) { + context.handle( + _isAdminMeta, + isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta), + ); + } + if (data.containsKey('has_profile_image')) { + context.handle( + _hasProfileImageMeta, + hasProfileImage.isAcceptableOrUnknown( + data['has_profile_image']!, + _hasProfileImageMeta, + ), + ); + } + if (data.containsKey('profile_changed_at')) { + context.handle( + _profileChangedAtMeta, + profileChangedAt.isAcceptableOrUnknown( + data['profile_changed_at']!, + _profileChangedAtMeta, + ), + ); + } + if (data.containsKey('quota_size_in_bytes')) { + context.handle( + _quotaSizeInBytesMeta, + quotaSizeInBytes.isAcceptableOrUnknown( + data['quota_size_in_bytes']!, + _quotaSizeInBytesMeta, + ), + ); + } + if (data.containsKey('quota_usage_in_bytes')) { + context.handle( + _quotaUsageInBytesMeta, + quotaUsageInBytes.isAcceptableOrUnknown( + data['quota_usage_in_bytes']!, + _quotaUsageInBytesMeta, + ), + ); + } + if (data.containsKey('pin_code')) { + context.handle( + _pinCodeMeta, + pinCode.isAcceptableOrUnknown(data['pin_code']!, _pinCodeMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ), + quotaSizeInBytes: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + $AuthUserEntityTable createAlias(String alias) { + return $AuthUserEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converteravatarColor = + const i0.EnumIndexConverter(i2.AvatarColor.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final i2.AvatarColor avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['email'] = i0.Variable(email); + map['is_admin'] = i0.Variable(isAdmin); + map['has_profile_image'] = i0.Variable(hasProfileImage); + map['profile_changed_at'] = i0.Variable(profileChangedAt); + { + map['avatar_color'] = i0.Variable( + i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor), + ); + } + map['quota_size_in_bytes'] = i0.Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = i0.Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = i0.Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + i0.ValueSerializer? serializer, + }) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromJson( + serializer.fromJson(json['avatarColor']), + ), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson( + i1.$AuthUserEntityTable.$converteravatarColor.toJson(avatarColor), + ), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + i1.AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + i2.AvatarColor? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + i0.Value pinCode = const i0.Value.absent(), + }) => i1.AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(i1.AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value email; + final i0.Value isAdmin; + final i0.Value hasProfileImage; + final i0.Value profileChangedAt; + final i0.Value avatarColor; + final i0.Value quotaSizeInBytes; + final i0.Value quotaUsageInBytes; + final i0.Value pinCode; + const AuthUserEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.email = const i0.Value.absent(), + this.isAdmin = const i0.Value.absent(), + this.hasProfileImage = const i0.Value.absent(), + this.profileChangedAt = const i0.Value.absent(), + this.avatarColor = const i0.Value.absent(), + this.quotaSizeInBytes = const i0.Value.absent(), + this.quotaUsageInBytes = const i0.Value.absent(), + this.pinCode = const i0.Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const i0.Value.absent(), + this.hasProfileImage = const i0.Value.absent(), + this.profileChangedAt = const i0.Value.absent(), + required i2.AvatarColor avatarColor, + this.quotaSizeInBytes = const i0.Value.absent(), + this.quotaUsageInBytes = const i0.Value.absent(), + this.pinCode = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + email = i0.Value(email), + avatarColor = i0.Value(avatarColor); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? email, + i0.Expression? isAdmin, + i0.Expression? hasProfileImage, + i0.Expression? profileChangedAt, + i0.Expression? avatarColor, + i0.Expression? quotaSizeInBytes, + i0.Expression? quotaUsageInBytes, + i0.Expression? pinCode, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + i1.AuthUserEntityCompanion copyWith({ + i0.Value? id, + i0.Value? name, + i0.Value? email, + i0.Value? isAdmin, + i0.Value? hasProfileImage, + i0.Value? profileChangedAt, + i0.Value? avatarColor, + i0.Value? quotaSizeInBytes, + i0.Value? quotaUsageInBytes, + i0.Value? pinCode, + }) { + return i1.AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @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 (email.present) { + map['email'] = i0.Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = i0.Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = i0.Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = i0.Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = i0.Variable( + i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor.value), + ); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = i0.Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = i0.Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = i0.Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index c796a12956..707d3326a4 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -1,5 +1,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class LocalAlbumEntity extends Table with DriftDefaultsMixin { @@ -11,9 +13,26 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin { IntColumn get backupSelection => intEnum()(); BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))(); + // // Linked album for putting assets to the remote album after finished uploading + TextColumn get linkedRemoteAlbumId => + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.setNull).nullable()(); + // Used for mark & sweep BoolColumn get marker_ => boolean().nullable()(); @override Set get primaryKey => {id}; } + +extension LocalAlbumEntityDataHelper on LocalAlbumEntityData { + LocalAlbum toDto({int assetCount = 0}) { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + backupSelection: backupSelection, + linkedRemoteAlbumId: linkedRemoteAlbumId, + ); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart index 5be349c8e0..0703844291 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart @@ -7,6 +7,9 @@ import 'package:immich_mobile/domain/models/album/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; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; typedef $$LocalAlbumEntityTableCreateCompanionBuilder = i1.LocalAlbumEntityCompanion Function({ @@ -15,6 +18,7 @@ typedef $$LocalAlbumEntityTableCreateCompanionBuilder = i0.Value updatedAt, required i2.BackupSelection backupSelection, i0.Value isIosSharedAlbum, + i0.Value linkedRemoteAlbumId, i0.Value marker_, }); typedef $$LocalAlbumEntityTableUpdateCompanionBuilder = @@ -24,9 +28,57 @@ typedef $$LocalAlbumEntityTableUpdateCompanionBuilder = i0.Value updatedAt, i0.Value backupSelection, i0.Value isIosSharedAlbum, + i0.Value linkedRemoteAlbumId, i0.Value marker_, }); +final class $$LocalAlbumEntityTableReferences + extends + i0.BaseReferences< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData + > { + $$LocalAlbumEntityTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static i5.$RemoteAlbumEntityTable _linkedRemoteAlbumIdTable( + i0.GeneratedDatabase db, + ) => i6.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .createAlias( + i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .linkedRemoteAlbumId, + i6.ReadDatabaseContainer( + db, + ).resultSet('remote_album_entity').id, + ), + ); + + i5.$$RemoteAlbumEntityTableProcessedTableManager? get linkedRemoteAlbumId { + final $_column = $_itemColumn('linked_remote_album_id'); + if ($_column == null) return null; + final manager = i5 + .$$RemoteAlbumEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer( + $_db, + ).resultSet('remote_album_entity'), + ) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_linkedRemoteAlbumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + class $$LocalAlbumEntityTableFilterComposer extends i0.Composer { $$LocalAlbumEntityTableFilterComposer({ @@ -66,6 +118,33 @@ class $$LocalAlbumEntityTableFilterComposer column: $table.marker_, builder: (column) => i0.ColumnFilters(column), ); + + i5.$$RemoteAlbumEntityTableFilterComposer get linkedRemoteAlbumId { + final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.linkedRemoteAlbumId, + referencedTable: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => i5.$$RemoteAlbumEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$LocalAlbumEntityTableOrderingComposer @@ -106,6 +185,34 @@ class $$LocalAlbumEntityTableOrderingComposer column: $table.marker_, builder: (column) => i0.ColumnOrderings(column), ); + + i5.$$RemoteAlbumEntityTableOrderingComposer get linkedRemoteAlbumId { + final i5.$$RemoteAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.linkedRemoteAlbumId, + referencedTable: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => i5.$$RemoteAlbumEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$LocalAlbumEntityTableAnnotationComposer @@ -139,6 +246,34 @@ class $$LocalAlbumEntityTableAnnotationComposer i0.GeneratedColumn get marker_ => $composableBuilder(column: $table.marker_, builder: (column) => column); + + i5.$$RemoteAlbumEntityTableAnnotationComposer get linkedRemoteAlbumId { + final i5.$$RemoteAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.linkedRemoteAlbumId, + referencedTable: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => i5.$$RemoteAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$LocalAlbumEntityTableTableManager @@ -152,16 +287,9 @@ class $$LocalAlbumEntityTableTableManager i1.$$LocalAlbumEntityTableAnnotationComposer, $$LocalAlbumEntityTableCreateCompanionBuilder, $$LocalAlbumEntityTableUpdateCompanionBuilder, - ( - i1.LocalAlbumEntityData, - i0.BaseReferences< - i0.GeneratedDatabase, - i1.$LocalAlbumEntityTable, - i1.LocalAlbumEntityData - >, - ), + (i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences), i1.LocalAlbumEntityData, - i0.PrefetchHooks Function() + i0.PrefetchHooks Function({bool linkedRemoteAlbumId}) > { $$LocalAlbumEntityTableTableManager( i0.GeneratedDatabase db, @@ -187,6 +315,7 @@ class $$LocalAlbumEntityTableTableManager i0.Value backupSelection = const i0.Value.absent(), i0.Value isIosSharedAlbum = const i0.Value.absent(), + i0.Value linkedRemoteAlbumId = const i0.Value.absent(), i0.Value marker_ = const i0.Value.absent(), }) => i1.LocalAlbumEntityCompanion( id: id, @@ -194,6 +323,7 @@ class $$LocalAlbumEntityTableTableManager updatedAt: updatedAt, backupSelection: backupSelection, isIosSharedAlbum: isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId, marker_: marker_, ), createCompanionCallback: @@ -203,6 +333,7 @@ class $$LocalAlbumEntityTableTableManager i0.Value updatedAt = const i0.Value.absent(), required i2.BackupSelection backupSelection, i0.Value isIosSharedAlbum = const i0.Value.absent(), + i0.Value linkedRemoteAlbumId = const i0.Value.absent(), i0.Value marker_ = const i0.Value.absent(), }) => i1.LocalAlbumEntityCompanion.insert( id: id, @@ -210,12 +341,60 @@ class $$LocalAlbumEntityTableTableManager updatedAt: updatedAt, backupSelection: backupSelection, isIosSharedAlbum: isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId, marker_: marker_, ), withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .map( + (e) => ( + e.readTable(table), + i1.$$LocalAlbumEntityTableReferences(db, table, e), + ), + ) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({linkedRemoteAlbumId = 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 (linkedRemoteAlbumId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.linkedRemoteAlbumId, + referencedTable: i1 + .$$LocalAlbumEntityTableReferences + ._linkedRemoteAlbumIdTable(db), + referencedColumn: i1 + .$$LocalAlbumEntityTableReferences + ._linkedRemoteAlbumIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, ), ); } @@ -230,16 +409,9 @@ typedef $$LocalAlbumEntityTableProcessedTableManager = i1.$$LocalAlbumEntityTableAnnotationComposer, $$LocalAlbumEntityTableCreateCompanionBuilder, $$LocalAlbumEntityTableUpdateCompanionBuilder, - ( - i1.LocalAlbumEntityData, - i0.BaseReferences< - i0.GeneratedDatabase, - i1.$LocalAlbumEntityTable, - i1.LocalAlbumEntityData - >, - ), + (i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences), i1.LocalAlbumEntityData, - i0.PrefetchHooks Function() + i0.PrefetchHooks Function({bool linkedRemoteAlbumId}) >; class $LocalAlbumEntityTable extends i3.LocalAlbumEntity @@ -308,6 +480,20 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity ), defaultValue: const i4.Constant(false), ); + static const i0.VerificationMeta _linkedRemoteAlbumIdMeta = + const i0.VerificationMeta('linkedRemoteAlbumId'); + @override + late final i0.GeneratedColumn linkedRemoteAlbumId = + i0.GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta( 'marker_', ); @@ -329,6 +515,7 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity updatedAt, backupSelection, isIosSharedAlbum, + linkedRemoteAlbumId, marker_, ]; @override @@ -371,6 +558,15 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity ), ); } + if (data.containsKey('linked_remote_album_id')) { + context.handle( + _linkedRemoteAlbumIdMeta, + linkedRemoteAlbumId.isAcceptableOrUnknown( + data['linked_remote_album_id']!, + _linkedRemoteAlbumIdMeta, + ), + ); + } if (data.containsKey('marker')) { context.handle( _marker_Meta, @@ -412,6 +608,10 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity i0.DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'], )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), marker_: attachedDatabase.typeMapping.read( i0.DriftSqlType.bool, data['${effectivePrefix}marker'], @@ -441,6 +641,7 @@ class LocalAlbumEntityData extends i0.DataClass final DateTime updatedAt; final i2.BackupSelection backupSelection; final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; final bool? marker_; const LocalAlbumEntityData({ required this.id, @@ -448,6 +649,7 @@ class LocalAlbumEntityData extends i0.DataClass required this.updatedAt, required this.backupSelection, required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, this.marker_, }); @override @@ -464,6 +666,9 @@ class LocalAlbumEntityData extends i0.DataClass ); } map['is_ios_shared_album'] = i0.Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = i0.Variable(linkedRemoteAlbumId); + } if (!nullToAbsent || marker_ != null) { map['marker'] = i0.Variable(marker_); } @@ -482,6 +687,9 @@ class LocalAlbumEntityData extends i0.DataClass backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection .fromJson(serializer.fromJson(json['backupSelection'])), isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), marker_: serializer.fromJson(json['marker_']), ); } @@ -498,6 +706,7 @@ class LocalAlbumEntityData extends i0.DataClass ), ), 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), 'marker_': serializer.toJson(marker_), }; } @@ -508,6 +717,7 @@ class LocalAlbumEntityData extends i0.DataClass DateTime? updatedAt, i2.BackupSelection? backupSelection, bool? isIosSharedAlbum, + i0.Value linkedRemoteAlbumId = const i0.Value.absent(), i0.Value marker_ = const i0.Value.absent(), }) => i1.LocalAlbumEntityData( id: id ?? this.id, @@ -515,6 +725,9 @@ class LocalAlbumEntityData extends i0.DataClass updatedAt: updatedAt ?? this.updatedAt, backupSelection: backupSelection ?? this.backupSelection, isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, marker_: marker_.present ? marker_.value : this.marker_, ); LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { @@ -528,6 +741,9 @@ class LocalAlbumEntityData extends i0.DataClass isIosSharedAlbum: data.isIosSharedAlbum.present ? data.isIosSharedAlbum.value : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, marker_: data.marker_.present ? data.marker_.value : this.marker_, ); } @@ -540,6 +756,7 @@ class LocalAlbumEntityData extends i0.DataClass ..write('updatedAt: $updatedAt, ') ..write('backupSelection: $backupSelection, ') ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') ..write('marker_: $marker_') ..write(')')) .toString(); @@ -552,6 +769,7 @@ class LocalAlbumEntityData extends i0.DataClass updatedAt, backupSelection, isIosSharedAlbum, + linkedRemoteAlbumId, marker_, ); @override @@ -563,6 +781,7 @@ class LocalAlbumEntityData extends i0.DataClass other.updatedAt == this.updatedAt && other.backupSelection == this.backupSelection && other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && other.marker_ == this.marker_); } @@ -573,6 +792,7 @@ class LocalAlbumEntityCompanion final i0.Value updatedAt; final i0.Value backupSelection; final i0.Value isIosSharedAlbum; + final i0.Value linkedRemoteAlbumId; final i0.Value marker_; const LocalAlbumEntityCompanion({ this.id = const i0.Value.absent(), @@ -580,6 +800,7 @@ class LocalAlbumEntityCompanion this.updatedAt = const i0.Value.absent(), this.backupSelection = const i0.Value.absent(), this.isIosSharedAlbum = const i0.Value.absent(), + this.linkedRemoteAlbumId = const i0.Value.absent(), this.marker_ = const i0.Value.absent(), }); LocalAlbumEntityCompanion.insert({ @@ -588,6 +809,7 @@ class LocalAlbumEntityCompanion this.updatedAt = const i0.Value.absent(), required i2.BackupSelection backupSelection, this.isIosSharedAlbum = const i0.Value.absent(), + this.linkedRemoteAlbumId = const i0.Value.absent(), this.marker_ = const i0.Value.absent(), }) : id = i0.Value(id), name = i0.Value(name), @@ -598,6 +820,7 @@ class LocalAlbumEntityCompanion i0.Expression? updatedAt, i0.Expression? backupSelection, i0.Expression? isIosSharedAlbum, + i0.Expression? linkedRemoteAlbumId, i0.Expression? marker_, }) { return i0.RawValuesInsertable({ @@ -606,6 +829,8 @@ class LocalAlbumEntityCompanion if (updatedAt != null) 'updated_at': updatedAt, if (backupSelection != null) 'backup_selection': backupSelection, if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, if (marker_ != null) 'marker': marker_, }); } @@ -616,6 +841,7 @@ class LocalAlbumEntityCompanion i0.Value? updatedAt, i0.Value? backupSelection, i0.Value? isIosSharedAlbum, + i0.Value? linkedRemoteAlbumId, i0.Value? marker_, }) { return i1.LocalAlbumEntityCompanion( @@ -624,6 +850,7 @@ class LocalAlbumEntityCompanion updatedAt: updatedAt ?? this.updatedAt, backupSelection: backupSelection ?? this.backupSelection, isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, marker_: marker_ ?? this.marker_, ); } @@ -650,6 +877,11 @@ class LocalAlbumEntityCompanion if (isIosSharedAlbum.present) { map['is_ios_shared_album'] = i0.Variable(isIosSharedAlbum.value); } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = i0.Variable( + linkedRemoteAlbumId.value, + ); + } if (marker_.present) { map['marker'] = i0.Variable(marker_.value); } @@ -664,6 +896,7 @@ class LocalAlbumEntityCompanion ..write('updatedAt: $updatedAt, ') ..write('backupSelection: $backupSelection, ') ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') ..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 index 8de879a09d..53f1a10662 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -10,6 +10,9 @@ class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { TextColumn get albumId => text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); + // Used for mark & sweep + BoolColumn get marker_ => boolean().nullable()(); + @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 index 78da361f62..70c298332b 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -15,11 +15,13 @@ typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder = i1.LocalAlbumAssetEntityCompanion Function({ required String assetId, required String albumId, + i0.Value marker_, }); typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder = i1.LocalAlbumAssetEntityCompanion Function({ i0.Value assetId, i0.Value albumId, + i0.Value marker_, }); final class $$LocalAlbumAssetEntityTableReferences @@ -113,6 +115,11 @@ class $$LocalAlbumAssetEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + i0.ColumnFilters get marker_ => $composableBuilder( + column: $table.marker_, + builder: (column) => i0.ColumnFilters(column), + ); + i3.$$LocalAssetEntityTableFilterComposer get assetId { final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( composer: this, @@ -177,6 +184,11 @@ class $$LocalAlbumAssetEntityTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + i0.ColumnOrderings get marker_ => $composableBuilder( + column: $table.marker_, + builder: (column) => i0.ColumnOrderings(column), + ); + i3.$$LocalAssetEntityTableOrderingComposer get assetId { final i3.$$LocalAssetEntityTableOrderingComposer composer = $composerBuilder( @@ -243,6 +255,9 @@ class $$LocalAlbumAssetEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + i0.GeneratedColumn get marker_ => + $composableBuilder(column: $table.marker_, builder: (column) => column); + i3.$$LocalAssetEntityTableAnnotationComposer get assetId { final i3.$$LocalAssetEntityTableAnnotationComposer composer = $composerBuilder( @@ -344,16 +359,22 @@ class $$LocalAlbumAssetEntityTableTableManager ({ i0.Value assetId = const i0.Value.absent(), i0.Value albumId = const i0.Value.absent(), + i0.Value marker_ = const i0.Value.absent(), }) => i1.LocalAlbumAssetEntityCompanion( assetId: assetId, albumId: albumId, + marker_: marker_, ), createCompanionCallback: - ({required String assetId, required String albumId}) => - i1.LocalAlbumAssetEntityCompanion.insert( - assetId: assetId, - albumId: albumId, - ), + ({ + required String assetId, + required String albumId, + i0.Value marker_ = const i0.Value.absent(), + }) => i1.LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + marker_: marker_, + ), withReferenceMapper: (p0) => p0 .map( (e) => ( @@ -477,8 +498,22 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity 'REFERENCES local_album_entity (id) ON DELETE CASCADE', ), ); + static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta( + 'marker_', + ); @override - List get $columns => [assetId, albumId]; + 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 => [assetId, albumId, marker_]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -507,6 +542,12 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity } else if (isInserting) { context.missing(_albumIdMeta); } + if (data.containsKey('marker')) { + context.handle( + _marker_Meta, + marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta), + ); + } return context; } @@ -527,6 +568,10 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity i0.DriftSqlType.string, data['${effectivePrefix}album_id'], )!, + marker_: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), ); } @@ -545,15 +590,20 @@ class LocalAlbumAssetEntityData extends i0.DataClass implements i0.Insertable { final String assetId; final String albumId; + final bool? marker_; const LocalAlbumAssetEntityData({ required this.assetId, required this.albumId, + this.marker_, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['asset_id'] = i0.Variable(assetId); map['album_id'] = i0.Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = i0.Variable(marker_); + } return map; } @@ -565,6 +615,7 @@ class LocalAlbumAssetEntityData extends i0.DataClass return LocalAlbumAssetEntityData( assetId: serializer.fromJson(json['assetId']), albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), ); } @override @@ -573,20 +624,26 @@ class LocalAlbumAssetEntityData extends i0.DataClass return { 'assetId': serializer.toJson(assetId), 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), }; } - i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => - i1.LocalAlbumAssetEntityData( - assetId: assetId ?? this.assetId, - albumId: albumId ?? this.albumId, - ); + i1.LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + i0.Value marker_ = const i0.Value.absent(), + }) => i1.LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); 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, + marker_: data.marker_.present ? data.marker_.value : this.marker_, ); } @@ -594,51 +651,60 @@ class LocalAlbumAssetEntityData extends i0.DataClass String toString() { return (StringBuffer('LocalAlbumAssetEntityData(') ..write('assetId: $assetId, ') - ..write('albumId: $albumId') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(assetId, albumId); + int get hashCode => Object.hash(assetId, albumId, marker_); @override bool operator ==(Object other) => identical(this, other) || (other is i1.LocalAlbumAssetEntityData && other.assetId == this.assetId && - other.albumId == this.albumId); + other.albumId == this.albumId && + other.marker_ == this.marker_); } class LocalAlbumAssetEntityCompanion extends i0.UpdateCompanion { final i0.Value assetId; final i0.Value albumId; + final i0.Value marker_; const LocalAlbumAssetEntityCompanion({ this.assetId = const i0.Value.absent(), this.albumId = const i0.Value.absent(), + this.marker_ = const i0.Value.absent(), }); LocalAlbumAssetEntityCompanion.insert({ required String assetId, required String albumId, + this.marker_ = const i0.Value.absent(), }) : assetId = i0.Value(assetId), albumId = i0.Value(albumId); static i0.Insertable custom({ i0.Expression? assetId, i0.Expression? albumId, + i0.Expression? marker_, }) { return i0.RawValuesInsertable({ if (assetId != null) 'asset_id': assetId, if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, }); } i1.LocalAlbumAssetEntityCompanion copyWith({ i0.Value? assetId, i0.Value? albumId, + i0.Value? marker_, }) { return i1.LocalAlbumAssetEntityCompanion( assetId: assetId ?? this.assetId, albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, ); } @@ -651,6 +717,9 @@ class LocalAlbumAssetEntityCompanion if (albumId.present) { map['album_id'] = i0.Variable(albumId.value); } + if (marker_.present) { + map['marker'] = i0.Variable(marker_.value); + } return map; } @@ -658,7 +727,8 @@ class LocalAlbumAssetEntityCompanion String toString() { return (StringBuffer('LocalAlbumAssetEntityCompanion(') ..write('assetId: $assetId, ') - ..write('albumId: $albumId') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 3130e41dbb..8b253f83a3 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -20,8 +20,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { Set get primaryKey => {id}; } -extension LocalAssetEntityDataDomainEx on LocalAssetEntityData { - LocalAsset toDto() => LocalAsset( +extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { + LocalAsset toDto({String? remoteId}) => LocalAsset( id: id, name: name, checksum: checksum, @@ -32,7 +32,7 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData { isFavorite: isFavorite, height: height, width: width, - remoteId: null, + remoteId: remoteId, orientation: orientation, ); } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 4426974413..dcc885a2a9 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -49,7 +49,7 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin } extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { - RemoteAsset toDto() => RemoteAsset( + RemoteAsset toDto({String? localId}) => RemoteAsset( id: id, name: name, ownerId: ownerId, @@ -64,7 +64,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { thumbHash: thumbHash, visibility: visibility, livePhotoVideoId: livePhotoVideoId, - localId: null, + localId: localId, stackId: stackId, ); } diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 78fc76b45d..667a9d6a59 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,6 +1,5 @@ import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:isar/isar.dart'; @@ -44,7 +43,7 @@ class User { static User fromDto(UserDto dto) => User( id: dto.id, - updatedAt: dto.updatedAt, + updatedAt: dto.updatedAt ?? DateTime(2025), email: dto.email, name: dto.name, isAdmin: dto.isAdmin, @@ -54,6 +53,8 @@ class User { avatarColor: dto.avatarColor, memoryEnabled: dto.memoryEnabled, inTimeline: dto.inTimeline, + quotaUsageInBytes: dto.quotaUsageInBytes, + quotaSizeInBytes: dto.quotaSizeInBytes, ); UserDto toDto() => UserDto( @@ -79,13 +80,12 @@ class UserEntity extends Table with DriftDefaultsMixin { TextColumn get id => text()(); TextColumn get name => text()(); - BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); TextColumn get email => text()(); + // Profile image BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))(); DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)(); - - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get avatarColor => intEnum().withDefault(const Constant(0))(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart index dbfddab4a0..083c14a095 100644 --- a/mobile/lib/infrastructure/entities/user.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user.entity.drift.dart @@ -3,28 +3,27 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i1; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; +import 'package:immich_mobile/domain/models/user.model.dart' 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; typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion Function({ required String id, required String name, - i0.Value isAdmin, required String email, i0.Value hasProfileImage, i0.Value profileChangedAt, - i0.Value updatedAt, + i0.Value avatarColor, }); typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion Function({ i0.Value id, i0.Value name, - i0.Value isAdmin, i0.Value email, i0.Value hasProfileImage, i0.Value profileChangedAt, - i0.Value updatedAt, + i0.Value avatarColor, }); class $$UserEntityTableFilterComposer @@ -46,11 +45,6 @@ class $$UserEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); - i0.ColumnFilters get isAdmin => $composableBuilder( - column: $table.isAdmin, - builder: (column) => i0.ColumnFilters(column), - ); - i0.ColumnFilters get email => $composableBuilder( column: $table.email, builder: (column) => i0.ColumnFilters(column), @@ -66,9 +60,10 @@ class $$UserEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); - i0.ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, - builder: (column) => i0.ColumnFilters(column), + i0.ColumnWithTypeConverterFilters + get avatarColor => $composableBuilder( + column: $table.avatarColor, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), ); } @@ -91,11 +86,6 @@ class $$UserEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); - i0.ColumnOrderings get isAdmin => $composableBuilder( - column: $table.isAdmin, - builder: (column) => i0.ColumnOrderings(column), - ); - i0.ColumnOrderings get email => $composableBuilder( column: $table.email, builder: (column) => i0.ColumnOrderings(column), @@ -111,8 +101,8 @@ class $$UserEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); - i0.ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, + i0.ColumnOrderings get avatarColor => $composableBuilder( + column: $table.avatarColor, builder: (column) => i0.ColumnOrderings(column), ); } @@ -132,9 +122,6 @@ class $$UserEntityTableAnnotationComposer i0.GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); - i0.GeneratedColumn get isAdmin => - $composableBuilder(column: $table.isAdmin, builder: (column) => column); - i0.GeneratedColumn get email => $composableBuilder(column: $table.email, builder: (column) => column); @@ -148,8 +135,11 @@ class $$UserEntityTableAnnotationComposer builder: (column) => column, ); - i0.GeneratedColumn get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => column); + i0.GeneratedColumnWithTypeConverter get avatarColor => + $composableBuilder( + column: $table.avatarColor, + builder: (column) => column, + ); } class $$UserEntityTableTableManager @@ -191,37 +181,33 @@ class $$UserEntityTableTableManager ({ 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(), i0.Value hasProfileImage = const i0.Value.absent(), i0.Value profileChangedAt = const i0.Value.absent(), - i0.Value updatedAt = const i0.Value.absent(), + i0.Value avatarColor = const i0.Value.absent(), }) => i1.UserEntityCompanion( id: id, name: name, - isAdmin: isAdmin, email: email, hasProfileImage: hasProfileImage, profileChangedAt: profileChangedAt, - updatedAt: updatedAt, + avatarColor: avatarColor, ), createCompanionCallback: ({ required String id, required String name, - i0.Value isAdmin = const i0.Value.absent(), required String email, i0.Value hasProfileImage = const i0.Value.absent(), i0.Value profileChangedAt = const i0.Value.absent(), - i0.Value updatedAt = const i0.Value.absent(), + i0.Value avatarColor = const i0.Value.absent(), }) => i1.UserEntityCompanion.insert( id: id, name: name, - isAdmin: isAdmin, email: email, hasProfileImage: hasProfileImage, profileChangedAt: profileChangedAt, - updatedAt: updatedAt, + avatarColor: avatarColor, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -253,7 +239,7 @@ typedef $$UserEntityTableProcessedTableManager = i0.PrefetchHooks Function() >; -class $UserEntityTable extends i2.UserEntity +class $UserEntityTable extends i3.UserEntity with i0.TableInfo<$UserEntityTable, i1.UserEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -279,21 +265,6 @@ class $UserEntityTable extends i2.UserEntity type: i0.DriftSqlType.string, requiredDuringInsert: true, ); - static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta( - 'isAdmin', - ); - @override - late final i0.GeneratedColumn isAdmin = i0.GeneratedColumn( - 'is_admin', - aliasedName, - false, - type: i0.DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'CHECK ("is_admin" IN (0, 1))', - ), - defaultValue: const i3.Constant(false), - ); static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta( 'email', ); @@ -318,7 +289,7 @@ class $UserEntityTable extends i2.UserEntity defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'CHECK ("has_profile_image" IN (0, 1))', ), - defaultValue: const i3.Constant(false), + defaultValue: const i4.Constant(false), ); static const i0.VerificationMeta _profileChangedAtMeta = const i0.VerificationMeta('profileChangedAt'); @@ -330,30 +301,26 @@ class $UserEntityTable extends i2.UserEntity false, type: i0.DriftSqlType.dateTime, requiredDuringInsert: false, - defaultValue: i3.currentDateAndTime, + 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: i3.currentDateAndTime, - ); + late final i0.GeneratedColumnWithTypeConverter + avatarColor = i0.GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0), + ).withConverter(i1.$UserEntityTable.$converteravatarColor); @override List get $columns => [ id, name, - isAdmin, email, hasProfileImage, profileChangedAt, - updatedAt, + avatarColor, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -380,12 +347,6 @@ class $UserEntityTable extends i2.UserEntity } else if (isInserting) { context.missing(_nameMeta); } - if (data.containsKey('is_admin')) { - context.handle( - _isAdminMeta, - isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta), - ); - } if (data.containsKey('email')) { context.handle( _emailMeta, @@ -412,12 +373,6 @@ class $UserEntityTable extends i2.UserEntity ), ); } - if (data.containsKey('updated_at')) { - context.handle( - _updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), - ); - } return context; } @@ -435,10 +390,6 @@ class $UserEntityTable extends i2.UserEntity i0.DriftSqlType.string, data['${effectivePrefix}name'], )!, - isAdmin: attachedDatabase.typeMapping.read( - i0.DriftSqlType.bool, - data['${effectivePrefix}is_admin'], - )!, email: attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}email'], @@ -451,10 +402,12 @@ class $UserEntityTable extends i2.UserEntity i0.DriftSqlType.dateTime, data['${effectivePrefix}profile_changed_at'], )!, - updatedAt: attachedDatabase.typeMapping.read( - i0.DriftSqlType.dateTime, - data['${effectivePrefix}updated_at'], - )!, + avatarColor: i1.$UserEntityTable.$converteravatarColor.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ), ); } @@ -463,6 +416,8 @@ class $UserEntityTable extends i2.UserEntity return $UserEntityTable(attachedDatabase, alias); } + static i0.JsonTypeConverter2 $converteravatarColor = + const i0.EnumIndexConverter(i2.AvatarColor.values); @override bool get withoutRowId => true; @override @@ -473,30 +428,31 @@ class UserEntityData extends i0.DataClass implements i0.Insertable { final String id; final String name; - final bool isAdmin; final String email; final bool hasProfileImage; final DateTime profileChangedAt; - final DateTime updatedAt; + final i2.AvatarColor avatarColor; const UserEntityData({ required this.id, required this.name, - required this.isAdmin, required this.email, required this.hasProfileImage, required this.profileChangedAt, - required this.updatedAt, + required this.avatarColor, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = i0.Variable(id); map['name'] = i0.Variable(name); - map['is_admin'] = i0.Variable(isAdmin); map['email'] = i0.Variable(email); map['has_profile_image'] = i0.Variable(hasProfileImage); map['profile_changed_at'] = i0.Variable(profileChangedAt); - map['updated_at'] = i0.Variable(updatedAt); + { + map['avatar_color'] = i0.Variable( + i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor), + ); + } return map; } @@ -508,11 +464,12 @@ class UserEntityData extends i0.DataClass return UserEntityData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), - isAdmin: serializer.fromJson(json['isAdmin']), email: serializer.fromJson(json['email']), hasProfileImage: serializer.fromJson(json['hasProfileImage']), profileChangedAt: serializer.fromJson(json['profileChangedAt']), - updatedAt: serializer.fromJson(json['updatedAt']), + avatarColor: i1.$UserEntityTable.$converteravatarColor.fromJson( + serializer.fromJson(json['avatarColor']), + ), ); } @override @@ -521,36 +478,34 @@ class UserEntityData extends i0.DataClass return { 'id': serializer.toJson(id), 'name': serializer.toJson(name), - 'isAdmin': serializer.toJson(isAdmin), 'email': serializer.toJson(email), 'hasProfileImage': serializer.toJson(hasProfileImage), 'profileChangedAt': serializer.toJson(profileChangedAt), - 'updatedAt': serializer.toJson(updatedAt), + 'avatarColor': serializer.toJson( + i1.$UserEntityTable.$converteravatarColor.toJson(avatarColor), + ), }; } i1.UserEntityData copyWith({ String? id, String? name, - bool? isAdmin, String? email, bool? hasProfileImage, DateTime? profileChangedAt, - DateTime? updatedAt, + i2.AvatarColor? avatarColor, }) => i1.UserEntityData( id: id ?? this.id, name: name ?? this.name, - isAdmin: isAdmin ?? this.isAdmin, email: email ?? this.email, hasProfileImage: hasProfileImage ?? this.hasProfileImage, profileChangedAt: profileChangedAt ?? this.profileChangedAt, - updatedAt: updatedAt ?? this.updatedAt, + avatarColor: avatarColor ?? this.avatarColor, ); UserEntityData copyWithCompanion(i1.UserEntityCompanion data) { return UserEntityData( id: data.id.present ? data.id.value : this.id, name: data.name.present ? data.name.value : this.name, - isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, email: data.email.present ? data.email.value : this.email, hasProfileImage: data.hasProfileImage.present ? data.hasProfileImage.value @@ -558,7 +513,9 @@ class UserEntityData extends i0.DataClass profileChangedAt: data.profileChangedAt.present ? data.profileChangedAt.value : this.profileChangedAt, - updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, ); } @@ -567,11 +524,10 @@ class UserEntityData extends i0.DataClass return (StringBuffer('UserEntityData(') ..write('id: $id, ') ..write('name: $name, ') - ..write('isAdmin: $isAdmin, ') ..write('email: $email, ') ..write('hasProfileImage: $hasProfileImage, ') ..write('profileChangedAt: $profileChangedAt, ') - ..write('updatedAt: $updatedAt') + ..write('avatarColor: $avatarColor') ..write(')')) .toString(); } @@ -580,11 +536,10 @@ class UserEntityData extends i0.DataClass int get hashCode => Object.hash( id, name, - isAdmin, email, hasProfileImage, profileChangedAt, - updatedAt, + avatarColor, ); @override bool operator ==(Object other) => @@ -592,78 +547,70 @@ class UserEntityData extends i0.DataClass (other is i1.UserEntityData && other.id == this.id && other.name == this.name && - other.isAdmin == this.isAdmin && other.email == this.email && other.hasProfileImage == this.hasProfileImage && other.profileChangedAt == this.profileChangedAt && - other.updatedAt == this.updatedAt); + other.avatarColor == this.avatarColor); } class UserEntityCompanion extends i0.UpdateCompanion { final i0.Value id; final i0.Value name; - final i0.Value isAdmin; final i0.Value email; final i0.Value hasProfileImage; final i0.Value profileChangedAt; - final i0.Value updatedAt; + final i0.Value avatarColor; const UserEntityCompanion({ this.id = const i0.Value.absent(), this.name = const i0.Value.absent(), - this.isAdmin = const i0.Value.absent(), this.email = const i0.Value.absent(), this.hasProfileImage = const i0.Value.absent(), this.profileChangedAt = const i0.Value.absent(), - this.updatedAt = const i0.Value.absent(), + this.avatarColor = const i0.Value.absent(), }); UserEntityCompanion.insert({ required String id, required String name, - this.isAdmin = const i0.Value.absent(), required String email, this.hasProfileImage = const i0.Value.absent(), this.profileChangedAt = const i0.Value.absent(), - this.updatedAt = const i0.Value.absent(), + this.avatarColor = const i0.Value.absent(), }) : id = i0.Value(id), name = i0.Value(name), email = i0.Value(email); static i0.Insertable custom({ i0.Expression? id, i0.Expression? name, - i0.Expression? isAdmin, i0.Expression? email, i0.Expression? hasProfileImage, i0.Expression? profileChangedAt, - i0.Expression? updatedAt, + i0.Expression? avatarColor, }) { return i0.RawValuesInsertable({ if (id != null) 'id': id, if (name != null) 'name': name, - if (isAdmin != null) 'is_admin': isAdmin, if (email != null) 'email': email, if (hasProfileImage != null) 'has_profile_image': hasProfileImage, if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, - if (updatedAt != null) 'updated_at': updatedAt, + if (avatarColor != null) 'avatar_color': avatarColor, }); } i1.UserEntityCompanion copyWith({ i0.Value? id, i0.Value? name, - i0.Value? isAdmin, i0.Value? email, i0.Value? hasProfileImage, i0.Value? profileChangedAt, - i0.Value? updatedAt, + i0.Value? avatarColor, }) { return i1.UserEntityCompanion( id: id ?? this.id, name: name ?? this.name, - isAdmin: isAdmin ?? this.isAdmin, email: email ?? this.email, hasProfileImage: hasProfileImage ?? this.hasProfileImage, profileChangedAt: profileChangedAt ?? this.profileChangedAt, - updatedAt: updatedAt ?? this.updatedAt, + avatarColor: avatarColor ?? this.avatarColor, ); } @@ -676,9 +623,6 @@ class UserEntityCompanion extends i0.UpdateCompanion { if (name.present) { map['name'] = i0.Variable(name.value); } - if (isAdmin.present) { - map['is_admin'] = i0.Variable(isAdmin.value); - } if (email.present) { map['email'] = i0.Variable(email.value); } @@ -688,8 +632,10 @@ class UserEntityCompanion extends i0.UpdateCompanion { if (profileChangedAt.present) { map['profile_changed_at'] = i0.Variable(profileChangedAt.value); } - if (updatedAt.present) { - map['updated_at'] = i0.Variable(updatedAt.value); + if (avatarColor.present) { + map['avatar_color'] = i0.Variable( + i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor.value), + ); } return map; } @@ -699,11 +645,10 @@ class UserEntityCompanion extends i0.UpdateCompanion { return (StringBuffer('UserEntityCompanion(') ..write('id: $id, ') ..write('name: $name, ') - ..write('isAdmin: $isAdmin, ') ..write('email: $email, ') ..write('hasProfileImage: $hasProfileImage, ') ..write('profileChangedAt: $profileChangedAt, ') - ..write('updatedAt: $updatedAt') + ..write('avatarColor: $avatarColor') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index f228f5de17..78f6b9479b 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -65,40 +65,53 @@ class RemoteImageRequest extends ImageRequest { return null; } - // Handle unknown content length from reverse proxy - final contentLength = response.contentLength; + final cacheManager = this.cacheManager; + final streamController = StreamController>(sync: true); + final Stream> stream; + cacheManager?.putStreamedFile(url, streamController.stream); + stream = response.map((chunk) { + if (_isCancelled) { + throw StateError('Cancelled request'); + } + if (cacheManager != null) { + streamController.add(chunk); + } + return chunk; + }); + + try { + final Uint8List bytes = await _downloadBytes(stream, response.contentLength); + streamController.close(); + return await ImmutableBuffer.fromUint8List(bytes); + } catch (e) { + streamController.addError(e); + streamController.close(); + if (_isCancelled) { + return null; + } + rethrow; + } + } + + Future _downloadBytes(Stream> stream, int length) async { final Uint8List bytes; int offset = 0; - - if (contentLength >= 0) { + if (length > 0) { // Known content length - use pre-allocated buffer - bytes = Uint8List(contentLength); - final subscription = response.listen((List chunk) { - // this is important to break the response stream if the request is cancelled - if (_isCancelled) { - throw StateError('Cancelled request'); - } + bytes = Uint8List(length); + await stream.listen((chunk) { bytes.setAll(offset, chunk); offset += chunk.length; - }, cancelOnError: true); - cacheManager?.putStreamedFile(url, response); - await subscription.asFuture(); + }, cancelOnError: true).asFuture(); } else { // Unknown content length - collect chunks dynamically final chunks = >[]; int totalLength = 0; - final subscription = response.listen((List chunk) { - // this is important to break the response stream if the request is cancelled - if (_isCancelled) { - throw StateError('Cancelled request'); - } + await stream.listen((chunk) { chunks.add(chunk); totalLength += chunk.length; - }, cancelOnError: true); - cacheManager?.putStreamedFile(url, response); - await subscription.asFuture(); + }, cancelOnError: true).asFuture(); - // Combine all chunks into a single buffer bytes = Uint8List(totalLength); for (final chunk in chunks) { bytes.setAll(offset, chunk); @@ -106,7 +119,7 @@ class RemoteImageRequest extends ImageRequest { } } - return await ImmutableBuffer.fromUint8List(bytes); + return bytes; } Future _loadCachedFile( diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart index d98067d5fd..0241711d4b 100644 --- a/mobile/lib/infrastructure/repositories/backup.repository.dart +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -4,9 +4,9 @@ import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import "package:immich_mobile/utils/database.utils.dart"; final backupRepositoryProvider = Provider( (ref) => DriftBackupRepository(ref.watch(driftProvider)), @@ -29,85 +29,59 @@ class DriftBackupRepository extends DriftDatabaseRepository { ..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.excluded)); } - Future getTotalCount() async { - final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) - ..addColumns([_db.localAlbumAssetEntity.assetId]) - ..join([ - innerJoin( - _db.localAlbumEntity, - _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), - useColumns: false, - ), - ]) - ..where( - _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & - _db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()), - ); + /// Returns all backup-related counts in a single query. + /// + /// - total: number of distinct assets in selected albums, excluding those that are also in excluded albums + /// - backup: number of those assets that already exist on the server for [userId] + /// - remainder: number of those assets that do not yet exist on the server for [userId] + /// (includes processing) + /// - processing: number of those assets that are still preparing/have a null checksum + Future<({int total, int remainder, int processing})> getAllCounts(String userId) async { + const sql = ''' + SELECT + COUNT(*) AS total_count, + COUNT(*) FILTER (WHERE lae.checksum IS NULL) AS processing_count, + COUNT(*) FILTER (WHERE rae.id IS NULL) AS remainder_count + FROM local_asset_entity lae + LEFT JOIN main.remote_asset_entity rae + ON lae.checksum = rae.checksum AND rae.owner_id = ?1 + WHERE EXISTS ( + SELECT 1 + FROM local_album_asset_entity laa + INNER JOIN main.local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id + AND la.backup_selection = ?2 + ) + AND NOT EXISTS ( + SELECT 1 + FROM local_album_asset_entity laa + INNER JOIN main.local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id + AND la.backup_selection = ?3 + ); + '''; - return query.get().then((rows) => rows.length); + final row = await _db + .customSelect( + sql, + variables: [ + Variable.withString(userId), + Variable.withInt(BackupSelection.selected.index), + Variable.withInt(BackupSelection.excluded.index), + ], + readsFrom: {_db.localAlbumAssetEntity, _db.localAlbumEntity, _db.localAssetEntity, _db.remoteAssetEntity}, + ) + .getSingle(); + + final data = row.data; + return ( + total: (data['total_count'] as int?) ?? 0, + remainder: (data['remainder_count'] as int?) ?? 0, + processing: (data['processing_count'] as int?) ?? 0, + ); } - Future getRemainderCount(String userId) async { - final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) - ..addColumns([_db.localAlbumAssetEntity.assetId]) - ..join([ - innerJoin( - _db.localAlbumEntity, - _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), - useColumns: false, - ), - innerJoin( - _db.localAssetEntity, - _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), - useColumns: false, - ), - leftOuterJoin( - _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) & - _db.remoteAssetEntity.ownerId.equals(userId), - useColumns: false, - ), - ]) - ..where( - _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & - _db.remoteAssetEntity.id.isNull() & - _db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()), - ); - - return query.get().then((rows) => rows.length); - } - - Future getBackupCount(String userId) async { - final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) - ..addColumns([_db.localAlbumAssetEntity.assetId]) - ..join([ - innerJoin( - _db.localAlbumEntity, - _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), - useColumns: false, - ), - innerJoin( - _db.localAssetEntity, - _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), - useColumns: false, - ), - innerJoin( - _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), - useColumns: false, - ), - ]) - ..where( - _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & - _db.remoteAssetEntity.id.isNotNull() & - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()), - ); - - return query.get().then((rows) => rows.length); - } - - Future> getCandidates(String userId) async { + Future> getCandidates(String userId, {bool onlyHashed = true}) async { final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true) ..addColumns([_db.localAlbumEntity.id]) ..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected)); @@ -115,7 +89,6 @@ class DriftBackupRepository extends DriftDatabaseRepository { final query = _db.localAssetEntity.select() ..where( (lae) => - lae.checksum.isNotNull() & existsQuery( _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) @@ -135,24 +108,10 @@ class DriftBackupRepository extends DriftDatabaseRepository { ) ..orderBy([(localAsset) => OrderingTerm.desc(localAsset.createdAt)]); + if (onlyHashed) { + query.where((lae) => lae.checksum.isNotNull()); + } + return query.map((localAsset) => localAsset.toDto()).get(); } - - FutureOr> getSourceAlbums(String localAssetId) { - final query = _db.localAlbumEntity.select() - ..where( - (lae) => - existsQuery( - _db.localAlbumAssetEntity.selectOnly() - ..addColumns([_db.localAlbumAssetEntity.albumId]) - ..where( - _db.localAlbumAssetEntity.albumId.equalsExp(lae.id) & - _db.localAlbumAssetEntity.assetId.equals(localAssetId), - ), - ) & - lae.backupSelection.equalsValue(BackupSelection.selected), - ) - ..orderBy([(lae) => OrderingTerm.asc(lae.name)]); - return query.map((localAlbum) => localAlbum.toDto()).get(); - } } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 386de2269e..7291c3a97b 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; @@ -43,6 +44,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { @DriftDatabase( tables: [ + AuthUserEntity, UserEntity, UserMetadataEntity, PartnerEntity, @@ -67,8 +69,31 @@ class Drift extends $Drift implements IDatabaseRepository { Drift([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); + Future reset() async { + // https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94 + await exclusively(() async { + // https://stackoverflow.com/a/65743498/25690041 + await customStatement('PRAGMA writable_schema = 1;'); + await customStatement('DELETE FROM sqlite_master;'); + await customStatement('VACUUM;'); + await customStatement('PRAGMA writable_schema = 0;'); + await customStatement('PRAGMA integrity_check'); + + await customStatement('PRAGMA user_version = 0'); + await beforeOpen( + // ignore: invalid_use_of_internal_member + resolvedEngine.executor, + OpeningDetails(null, schemaVersion), + ); + await customStatement('PRAGMA user_version = $schemaVersion'); + + // Refresh all stream queries + notifyUpdates({for (final table in allTables) TableUpdate.onTable(table)}); + }); + } + @override - int get schemaVersion => 8; + int get schemaVersion => 12; @override MigrationStrategy get migration => MigrationStrategy( @@ -123,6 +148,36 @@ class Drift extends $Drift implements IDatabaseRepository { from7To8: (m, v8) async { await m.create(v8.storeEntity); }, + from8To9: (m, v9) async { + await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId); + }, + from9To10: (m, v10) async { + await m.createTable(v10.authUserEntity); + await m.addColumn(v10.userEntity, v10.userEntity.avatarColor); + await m.alterTable(TableMigration(v10.userEntity)); + }, + from10To11: (m, v11) async { + await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_); + }, + from11To12: (m, v12) async { + final localToUTCMapping = { + v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt], + v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt], + }; + + for (final entry in localToUTCMapping.entries) { + final table = entry.key; + await m.alterTable( + TableMigration( + table, + columnTransformer: { + for (final column in entry.value) + column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'), + }, + ), + ); + } + }, ), ); @@ -138,7 +193,7 @@ class Drift extends $Drift implements IDatabaseRepository { await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA synchronous = NORMAL'); await customStatement('PRAGMA journal_mode = WAL'); - await customStatement('PRAGMA busy_timeout = 500'); + await customStatement('PRAGMA busy_timeout = 30000'); }, ); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 456296e2d0..e39ed8a560 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -9,35 +9,37 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' as i3; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' as i4; -import 'package:immich_mobile/infrastructure/entities/local_album.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/user_metadata.entity.drift.dart' - as i7; -import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' - as i8; -import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' - as i9; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i6; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i7; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart' + as i8; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' + as i9; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i10; -import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' as i11; -import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' as i12; -import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' as i13; -import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' as i14; -import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' as i15; -import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' as i16; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' as i17; -import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' as i18; -import 'package:drift/internal/modular.dart' as i19; +import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' + as i19; +import 'package:drift/internal/modular.dart' as i20; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -48,33 +50,36 @@ abstract class $Drift extends i0.GeneratedDatabase { late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this); late final i4.$LocalAssetEntityTable localAssetEntity = i4 .$LocalAssetEntityTable(this); - late final i5.$LocalAlbumEntityTable localAlbumEntity = i5 + late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5 + .$RemoteAlbumEntityTable(this); + late final i6.$LocalAlbumEntityTable localAlbumEntity = i6 .$LocalAlbumEntityTable(this); - late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i6 + late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7 .$LocalAlbumAssetEntityTable(this); - late final i7.$UserMetadataEntityTable userMetadataEntity = i7 - .$UserMetadataEntityTable(this); - late final i8.$PartnerEntityTable partnerEntity = i8.$PartnerEntityTable( + late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable( this, ); - late final i9.$RemoteExifEntityTable remoteExifEntity = i9 - .$RemoteExifEntityTable(this); - late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = i10 - .$RemoteAlbumEntityTable(this); - late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11 - .$RemoteAlbumAssetEntityTable(this); - late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12 - .$RemoteAlbumUserEntityTable(this); - late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this); - late final i14.$MemoryAssetEntityTable memoryAssetEntity = i14 - .$MemoryAssetEntityTable(this); - late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this); - late final i16.$AssetFaceEntityTable assetFaceEntity = i16 - .$AssetFaceEntityTable(this); - late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this); - i18.MergedAssetDrift get mergedAssetDrift => i19.ReadDatabaseContainer( + late final i9.$UserMetadataEntityTable userMetadataEntity = i9 + .$UserMetadataEntityTable(this); + late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable( this, - ).accessor(i18.MergedAssetDrift.new); + ); + late final i11.$RemoteExifEntityTable remoteExifEntity = i11 + .$RemoteExifEntityTable(this); + late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12 + .$RemoteAlbumAssetEntityTable(this); + late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13 + .$RemoteAlbumUserEntityTable(this); + late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this); + late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15 + .$MemoryAssetEntityTable(this); + late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this); + late final i17.$AssetFaceEntityTable assetFaceEntity = i17 + .$AssetFaceEntityTable(this); + late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this); + i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer( + this, + ).accessor(i19.MergedAssetDrift.new); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -84,6 +89,7 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteAssetEntity, stackEntity, localAssetEntity, + remoteAlbumEntity, localAlbumEntity, localAlbumAssetEntity, i4.idxLocalAssetChecksum, @@ -91,10 +97,10 @@ abstract class $Drift extends i0.GeneratedDatabase { i2.uQRemoteAssetsOwnerChecksum, i2.uQRemoteAssetsOwnerLibraryChecksum, i2.idxRemoteAssetChecksum, + authUserEntity, userMetadataEntity, partnerEntity, remoteExifEntity, - remoteAlbumEntity, remoteAlbumAssetEntity, remoteAlbumUserEntity, memoryEntity, @@ -102,7 +108,7 @@ abstract class $Drift extends i0.GeneratedDatabase { personEntity, assetFaceEntity, storeEntity, - i9.idxLatLng, + i11.idxLatLng, ]; @override i0.StreamQueryUpdateRules @@ -123,6 +129,33 @@ abstract class $Drift extends i0.GeneratedDatabase { ), result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: i0.UpdateKind.delete, + ), + result: [ + i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete, + ), + result: [ + i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: i0.UpdateKind.delete, + ), + result: [ + i0.TableUpdate('local_album_entity', kind: i0.UpdateKind.update), + ], + ), i0.WritePropagation( on: i0.TableUpdateQuery.onTableName( 'local_asset_entity', @@ -173,24 +206,6 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), ], ), - i0.WritePropagation( - on: i0.TableUpdateQuery.onTableName( - 'user_entity', - limitUpdateKind: i0.UpdateKind.delete, - ), - result: [ - i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete), - ], - ), - i0.WritePropagation( - on: i0.TableUpdateQuery.onTableName( - 'remote_asset_entity', - limitUpdateKind: i0.UpdateKind.delete, - ), - result: [ - i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update), - ], - ), i0.WritePropagation( on: i0.TableUpdateQuery.onTableName( 'remote_asset_entity', @@ -290,33 +305,35 @@ class $DriftManager { i3.$$StackEntityTableTableManager(_db, _db.stackEntity); i4.$$LocalAssetEntityTableTableManager get localAssetEntity => i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); - i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity => - i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); - i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 + i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => + i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); + i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7 .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); - i7.$$UserMetadataEntityTableTableManager get userMetadataEntity => - i7.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); - i8.$$PartnerEntityTableTableManager get partnerEntity => - i8.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); - i9.$$RemoteExifEntityTableTableManager get remoteExifEntity => - i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); - i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => - i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); - i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => - i11.$$RemoteAlbumAssetEntityTableTableManager( + i8.$$AuthUserEntityTableTableManager get authUserEntity => + i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity); + i9.$$UserMetadataEntityTableTableManager get userMetadataEntity => + i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); + i10.$$PartnerEntityTableTableManager get partnerEntity => + i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i11.$$RemoteExifEntityTableTableManager get remoteExifEntity => + i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); + i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => + i12.$$RemoteAlbumAssetEntityTableTableManager( _db, _db.remoteAlbumAssetEntity, ); - i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12 + i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13 .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); - i13.$$MemoryEntityTableTableManager get memoryEntity => - i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); - i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => - i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); - i15.$$PersonEntityTableTableManager get personEntity => - i15.$$PersonEntityTableTableManager(_db, _db.personEntity); - i16.$$AssetFaceEntityTableTableManager get assetFaceEntity => - i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); - i17.$$StoreEntityTableTableManager get storeEntity => - i17.$$StoreEntityTableTableManager(_db, _db.storeEntity); + i14.$$MemoryEntityTableTableManager get memoryEntity => + i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); + i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => + i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); + i16.$$PersonEntityTableTableManager get personEntity => + i16.$$PersonEntityTableTableManager(_db, _db.personEntity); + i17.$$AssetFaceEntityTableTableManager get assetFaceEntity => + i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); + i18.$$StoreEntityTableTableManager get storeEntity => + i18.$$StoreEntityTableTableManager(_db, _db.storeEntity); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 68c54174b4..c973cd6f13 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -3435,6 +3435,1608 @@ i1.GeneratedColumn _column_89(String aliasedName) => true, type: i1.DriftSqlType.int, ); + +final class Schema9 extends i0.VersionedSchema { + Schema9({required super.database}) : super(version: 9); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + late final Shape16 userEntity = Shape16( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_84, + _column_85, + _column_5, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape17 remoteAssetEntity = Shape17( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); +} + +class Shape19 extends i0.VersionedTable { + Shape19({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get backupSelection => + columnsByName['backup_selection']! as i1.GeneratedColumn; + i1.GeneratedColumn get isIosSharedAlbum => + columnsByName['is_ios_shared_album']! as i1.GeneratedColumn; + i1.GeneratedColumn get linkedRemoteAlbumId => + columnsByName['linked_remote_album_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get marker_ => + columnsByName['marker']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_90(String aliasedName) => + i1.GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + +final class Schema10 extends i0.VersionedSchema { + Schema10({required super.database}) : super(version: 10); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape17 remoteAssetEntity = Shape17( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); +} + +class Shape20 extends i0.VersionedTable { + Shape20({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get email => + columnsByName['email']! as i1.GeneratedColumn; + i1.GeneratedColumn get hasProfileImage => + columnsByName['has_profile_image']! as i1.GeneratedColumn; + i1.GeneratedColumn get profileChangedAt => + columnsByName['profile_changed_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get avatarColor => + columnsByName['avatar_color']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_91(String aliasedName) => + i1.GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: i1.DriftSqlType.int, + defaultValue: const CustomExpression('0'), + ); + +class Shape21 extends i0.VersionedTable { + Shape21({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get email => + columnsByName['email']! as i1.GeneratedColumn; + i1.GeneratedColumn get isAdmin => + columnsByName['is_admin']! as i1.GeneratedColumn; + i1.GeneratedColumn get hasProfileImage => + columnsByName['has_profile_image']! as i1.GeneratedColumn; + i1.GeneratedColumn get profileChangedAt => + columnsByName['profile_changed_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get avatarColor => + columnsByName['avatar_color']! as i1.GeneratedColumn; + i1.GeneratedColumn get quotaSizeInBytes => + columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn; + i1.GeneratedColumn get quotaUsageInBytes => + columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn; + i1.GeneratedColumn get pinCode => + columnsByName['pin_code']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_92(String aliasedName) => + i1.GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); +i1.GeneratedColumn _column_93(String aliasedName) => + i1.GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: i1.DriftSqlType.int, + defaultValue: const CustomExpression('0'), + ); +i1.GeneratedColumn _column_94(String aliasedName) => + i1.GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: i1.DriftSqlType.string, + ); + +final class Schema11 extends i0.VersionedSchema { + Schema11({required super.database}) : super(version: 11); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape17 remoteAssetEntity = Shape17( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); +} + +class Shape22 extends i0.VersionedTable { + Shape22({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => + columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get albumId => + columnsByName['album_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get marker_ => + columnsByName['marker']! as i1.GeneratedColumn; +} + +final class Schema12 extends i0.VersionedSchema { + Schema12({required super.database}) : super(version: 12); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape17 remoteAssetEntity = Shape17( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -3443,6 +5045,10 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, + required Future Function(i1.Migrator m, Schema11 schema) from10To11, + required Future Function(i1.Migrator m, Schema12 schema) from11To12, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -3481,6 +5087,26 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from7To8(migrator, schema); return 8; + case 8: + final schema = Schema9(database: database); + final migrator = i1.Migrator(database, schema); + await from8To9(migrator, schema); + return 9; + case 9: + final schema = Schema10(database: database); + final migrator = i1.Migrator(database, schema); + await from9To10(migrator, schema); + return 10; + case 10: + final schema = Schema11(database: database); + final migrator = i1.Migrator(database, schema); + await from10To11(migrator, schema); + return 11; + case 11: + final schema = Schema12(database: database); + final migrator = i1.Migrator(database, schema); + await from11To12(migrator, schema); + return 12; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -3495,6 +5121,10 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, + required Future Function(i1.Migrator m, Schema11 schema) from10To11, + required Future Function(i1.Migrator m, Schema12 schema) from11To12, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -3504,5 +5134,9 @@ i1.OnUpgrade stepByStep({ from5To6: from5To6, from6To7: from6To7, from7To8: from7To8, + from8To9: from8To9, + from9To10: from9To10, + from10To11: from10To11, + from11To12: from11To12, ), ); diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 0c29768880..e4bff24879 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -1,21 +1,20 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.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.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/utils/database.utils.dart'; -import 'package:platform/platform.dart'; enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset } class DriftLocalAlbumRepository extends DriftDatabaseRepository { final Drift _db; - final Platform _platform; - const DriftLocalAlbumRepository(this._db, {Platform? platform}) - : _platform = platform ?? const LocalPlatform(), - super(_db); + + const DriftLocalAlbumRepository(this._db) : super(_db); Future> getAll({Set sortBy = const {}}) { final assetCount = _db.localAlbumAssetEntity.assetId.count(); @@ -49,11 +48,18 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get(); } + Future> getBackupAlbums() async { + final query = _db.localAlbumEntity.select() + ..where((row) => row.backupSelection.equalsValue(BackupSelection.selected)); + + return query.map((row) => row.toDto()).get(); + } + 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 getAssetIds(albumId); + final assetsToDelete = CurrentPlatform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId); await _deleteAssets(assetsToDelete); await _db.managers.localAlbumEntity @@ -66,17 +72,33 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { 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); + return _db.transaction(() async { + await _db.managers.localAlbumAssetEntity + .filter((row) => row.albumId.id.equals(albumId)) + .update((album) => album(marker_: const Value(true))); + + await _db.batch((batch) { + for (final assetId in assetIdsToKeep) { + batch.update( + _db.localAlbumAssetEntity, + const LocalAlbumAssetEntityCompanion(marker_: Value(null)), + where: (row) => row.assetId.equals(assetId) & row.albumId.equals(albumId), + ); + } + }); + + final query = _db.localAssetEntity.delete() + ..where( + (row) => row.id.isInQuery( + _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where( + _db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAlbumAssetEntity.marker_.isNotNull(), + ), + ), + ); + await query.go(); }); - await deleteSmt.go(); } Future upsert( @@ -136,7 +158,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { } }); - if (_platform.isAndroid) { + if (CurrentPlatform.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 @@ -192,10 +214,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { // 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), - ); + for (final albumId in albumIds.cast().nonNulls) { + batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.albumId.equals(albumId) & f.assetId.equals(assetId)); + } }); }); await _db.batch((batch) async { @@ -257,7 +278,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return Future.value(); } - if (_platform.isAndroid) { + if (CurrentPlatform.isAndroid) { return _deleteAssets(assetIds); } @@ -282,12 +303,14 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return transaction(() async { if (assetsToUnLink.isNotEmpty) { - await _db.batch( - (batch) => batch.deleteWhere( - _db.localAlbumAssetEntity, - (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId), - ), - ); + await _db.batch((batch) { + for (final assetId in assetsToUnLink) { + batch.deleteWhere( + _db.localAlbumAssetEntity, + (row) => row.assetId.equals(assetId) & row.albumId.equals(albumId), + ); + } + }); } await _deleteAssets(assetsToDelete); @@ -314,7 +337,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { } return _db.batch((batch) { - batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids)); + for (final id in ids) { + batch.deleteWhere(_db.localAssetEntity, (row) => row.id.equals(id)); + } }); } @@ -335,4 +360,16 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { Future getCount() { return _db.managers.localAlbumEntity.count(); } + + Future unlinkRemoteAlbum(String id) async { + return _db.localAlbumEntity.update() + ..where((row) => row.id.equals(id)) + ..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null))); + } + + Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async { + return _db.localAlbumEntity.update() + ..where((row) => row.id.equals(localAlbumId)) + ..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId))); + } } diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 5865447064..2b76472c9e 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,6 +1,7 @@ -import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.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/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -26,19 +27,25 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { Future get(String id) => _assetSelectable(id).getSingleOrNull(); + Future> getByChecksum(String checksum) { + final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.equals(checksum)); + + return query.map((row) => row.toDto()).get(); + } + Stream watch(String id) => _assetSelectable(id).watchSingleOrNull(); - Future updateHashes(Iterable hashes) { + Future updateHashes(Map hashes) { if (hashes.isEmpty) { return Future.value(); } return _db.batch((batch) async { - for (final asset in hashes) { + for (final entry in hashes.entries) { batch.update( _db.localAssetEntity, - LocalAssetEntityCompanion(checksum: Value(asset.checksum)), - where: (e) => e.id.equals(asset.id), + LocalAssetEntityCompanion(checksum: Value(entry.value)), + where: (e) => e.id.equals(entry.key), ); } }); @@ -50,8 +57,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { } return _db.batch((batch) { - for (final slice in ids.slices(32000)) { - batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice)); + for (final id in ids) { + batch.deleteWhere(_db.localAssetEntity, (e) => e.id.equals(id)); } }); } @@ -69,4 +76,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { Future getHashedCount() { return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count(); } + + Future> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) { + final query = _db.localAlbumEntity.select() + ..where( + (lae) => existsQuery( + _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.albumId]) + ..where( + _db.localAlbumAssetEntity.albumId.equalsExp(lae.id) & + _db.localAlbumAssetEntity.assetId.equals(localAssetId), + ), + ), + ) + ..orderBy([(lae) => OrderingTerm.asc(lae.name)]); + if (backupSelection != null) { + query.where((lae) => lae.backupSelection.equalsValue(backupSelection)); + } + return query.map((localAlbum) => localAlbum.toDto()).get(); + } } diff --git a/mobile/lib/infrastructure/repositories/memory.repository.dart b/mobile/lib/infrastructure/repositories/memory.repository.dart index 2a52faf2dd..0dcf7200cc 100644 --- a/mobile/lib/infrastructure/repositories/memory.repository.dart +++ b/mobile/lib/infrastructure/repositories/memory.repository.dart @@ -15,8 +15,8 @@ class DriftMemoryRepository extends DriftDatabaseRepository { final query = _db.select(_db.memoryEntity).join([ - leftOuterJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)), - leftOuterJoin( + innerJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)), + innerJoin( _db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) & _db.remoteAssetEntity.deletedAt.isNull() & @@ -30,6 +30,9 @@ class DriftMemoryRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(_db.memoryEntity.memoryAt), OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]); final rows = await query.get(); + if (rows.isEmpty) { + return const []; + } final Map memoriesMap = {}; @@ -46,7 +49,7 @@ class DriftMemoryRepository extends DriftDatabaseRepository { } } - return memoriesMap.values.toList(); + return memoriesMap.values.toList(growable: false); } Future get(String memoryId) async { diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 44a288787e..22d4715c1e 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -62,7 +62,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .toDto( assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!, - isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, ), ) .get(); @@ -107,12 +107,21 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .toDto( assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!, - isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, ), ) .getSingleOrNull(); } + Future getByName(String albumName, String ownerId) { + final query = _db.remoteAlbumEntity.select() + ..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId)) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(1); + + return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull(); + } + Future create(RemoteAlbum album, List assetIds) async { await _db.transaction(() async { final entity = RemoteAlbumEntityCompanion( @@ -157,8 +166,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ); } - Future removeAssets(String albumId, List assetIds) { - return _db.remoteAlbumAssetEntity.deleteWhere((tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds)); + Future removeAssets(String albumId, List assetIds) { + return _db.batch((batch) { + for (final assetId in assetIds) { + batch.deleteWhere( + _db.remoteAlbumAssetEntity, + (row) => row.albumId.equals(albumId) & row.assetId.equals(assetId), + ); + } + }); } FutureOr<(DateTime, DateTime)> getDateRange(String albumId) { @@ -193,14 +209,13 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { id: user.id, email: user.email, name: user.name, - isAdmin: user.isAdmin, - updatedAt: user.updatedAt, memoryEnabled: true, inTimeline: false, isPartnerSharedBy: false, isPartnerSharedWith: false, profileChangedAt: user.profileChangedAt, hasProfileImage: user.hasProfileImage, + avatarColor: user.avatarColor, ), ) .get(); @@ -290,8 +305,9 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .readTable(_db.remoteAlbumEntity) .toDto( ownerName: row.read(_db.userEntity.name)!, - isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, ); + return album; }).watchSingleOrNull(); } @@ -321,6 +337,42 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { Future getCount() { return _db.managers.remoteAlbumEntity.count(); } + + Future> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async { + // Find remote asset ids that: + // 1. Belong to the provided local album (via local_album_asset_entity) + // 2. Have been uploaded (i.e. a matching remote asset exists for the same checksum & owner) + // 3. Are NOT already in the remote album (remote_album_asset_entity) + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([_db.remoteAssetEntity.id]) + ..join([ + innerJoin( + _db.localAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + useColumns: false, + ), + innerJoin( + _db.localAlbumAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + // Left join remote album assets to exclude those already in the remote album + leftOuterJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id) & + _db.remoteAlbumAssetEntity.albumId.equals(remoteAlbumId), + useColumns: false, + ), + ]) + ..where( + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteAssetEntity.deletedAt.isNull() & + _db.localAlbumAssetEntity.albumId.equals(localAlbumId) & + _db.remoteAlbumAssetEntity.assetId.isNull(), // only those not yet linked + ); + + return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get(); + } } extension on RemoteAlbumEntityData { diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 3ed7dddfe8..be55c21afc 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -55,13 +55,20 @@ class RemoteAssetRepository extends DriftDatabaseRepository { return _assetSelectable(id).getSingleOrNull(); } + Future getByChecksum(String checksum) { + final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum)); + + return query.map((row) => row.toDto()).getSingleOrNull(); + } + Future> getStackChildren(RemoteAsset asset) { - if (asset.stackId == null) { - return Future.value([]); + final stackId = asset.stackId; + if (stackId == null) { + return Future.value(const []); } final query = _db.remoteAssetEntity.select() - ..where((row) => row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not()) + ..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not()) ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]); return query.map((row) => row.toDto()).get(); @@ -74,9 +81,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { .getSingleOrNull(); } - Future> getPlaces() { + Future> getPlaces(String userId) { final asset = Subquery( - _db.remoteAssetEntity.select()..orderBy([(row) => OrderingTerm.desc(row.createdAt)]), + _db.remoteAssetEntity.select() + ..where((row) => row.ownerId.equals(userId)) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]), "asset", ); @@ -153,7 +162,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { } Future delete(List ids) { - return _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(ids)); + return _db.batch((batch) { + for (final id in ids) { + batch.deleteWhere(_db.remoteAssetEntity, (row) => row.id.equals(id)); + } + }); } Future updateLocation(List ids, LatLng location) { @@ -192,7 +205,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { .map((row) => row.id) .get(); - await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds)); + await _db.batch((batch) { + for (final stackId in stackIds) { + batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stackId)); + } + }); await _db.batch((batch) { final companion = StackEntityCompanion(ownerId: Value(userId), primaryAssetId: Value(stack.primaryAssetId)); @@ -212,15 +229,21 @@ class RemoteAssetRepository extends DriftDatabaseRepository { Future unStack(List stackIds) { return _db.transaction(() async { - await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds)); + await _db.batch((batch) { + for (final stackId in stackIds) { + batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stackId)); + } + }); // TODO: delete this after adding foreign key on stackId await _db.batch((batch) { - batch.update( - _db.remoteAssetEntity, - const RemoteAssetEntityCompanion(stackId: Value(null)), - where: (e) => e.stackId.isIn(stackIds), - ); + for (final stackId in stackIds) { + batch.update( + _db.remoteAssetEntity, + const RemoteAssetEntityCompanion(stackId: Value(null)), + where: (e) => e.stackId.equals(stackId), + ); + } }); }); } diff --git a/mobile/lib/infrastructure/repositories/search_api.repository.dart b/mobile/lib/infrastructure/repositories/search_api.repository.dart index 129746120b..dd72333398 100644 --- a/mobile/lib/infrastructure/repositories/search_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -33,7 +33,7 @@ class SearchApiRepository extends ApiRepository { personIds: filter.people.map((e) => e.id).toList(), type: type, page: page, - size: 1000, + size: 100, ), ); } diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 18302aeb7d..164fa04529 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -16,6 +16,13 @@ class StorageRepository { file = await entity?.originFile; if (file == null) { log.warning("Cannot get file for asset $assetId"); + return null; + } + + final exists = await file.exists(); + if (!exists) { + log.warning("File for asset $assetId does not exist"); + return null; } } catch (error, stackTrace) { log.warning("Error getting file for asset $assetId", error, stackTrace); @@ -34,6 +41,13 @@ class StorageRepository { log.warning( "Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", ); + return null; + } + + final exists = await file.exists(); + if (!exists) { + log.warning("Motion file for asset ${asset.id} does not exist"); + return null; } } catch (error, stackTrace) { log.warning( diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index 5aea631171..d4e34a02f5 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -173,7 +173,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo const (bool) => entity.intValue == 1, const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), const (UserDto) => - entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!), + entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!), _ => null, } as T?; @@ -184,7 +184,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo const (String) => (null, value as String), const (bool) => ((value as bool) ? 1 : 0, null), const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id), + const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id), _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), }; return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue)); diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 2175e77e82..8bf2e80579 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -3,7 +3,9 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -18,7 +20,8 @@ class SyncApiRepository { } Future streamChanges( - Function(List, Function() abort) onData, { + Future Function(List, Function() abort, Function() reset) onData, { + Function()? onReset, int batchSize = kSyncEventBatchSize, http.Client? httpClient, }) async { @@ -32,11 +35,13 @@ class SyncApiRepository { await _api.applyToParams([], headerParams); headers.addAll(headerParams); + final shouldReset = Store.get(StoreKey.shouldResetSync, false); final request = http.Request('POST', Uri.parse(endpoint)); request.headers.addAll(headers); request.body = jsonEncode( SyncStreamDto( types: [ + SyncRequestType.authUsersV1, SyncRequestType.usersV1, SyncRequestType.assetsV1, SyncRequestType.assetExifsV1, @@ -56,6 +61,7 @@ class SyncApiRepository { SyncRequestType.peopleV1, SyncRequestType.assetFacesV1, ], + reset: shouldReset, ).toJson(), ); @@ -69,6 +75,8 @@ class SyncApiRepository { shouldAbort = true; } + final reset = onReset ?? () {}; + try { final response = await client.send(request); @@ -77,6 +85,9 @@ class SyncApiRepository { throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody'); } + // Reset after successful stream start + await Store.put(StoreKey.shouldResetSync, false); + await for (final chunk in response.stream.transform(utf8.decoder)) { if (shouldAbort) { break; @@ -91,15 +102,14 @@ class SyncApiRepository { continue; } - await onData(_parseLines(lines), abort); + await onData(_parseLines(lines), abort, reset); lines.clear(); } if (lines.isNotEmpty && !shouldAbort) { - await onData(_parseLines(lines), abort); + await onData(_parseLines(lines), abort, reset); } } catch (error, stack) { - _logger.severe("Error processing stream", error, stack); return Future.error(error, stack); } finally { client.close(); @@ -130,6 +140,7 @@ class SyncApiRepository { } const _kResponseMap = { + SyncEntityType.authUserV1: SyncAuthUserV1.fromJson, SyncEntityType.userV1: SyncUserV1.fromJson, SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, SyncEntityType.partnerV1: SyncPartnerV1.fromJson, @@ -156,7 +167,8 @@ const _kResponseMap = { SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson, SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson, SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson, - SyncEntityType.syncAckV1: _SyncAckV1.fromJson, + SyncEntityType.syncAckV1: _SyncEmptyDto.fromJson, + SyncEntityType.syncResetV1: _SyncEmptyDto.fromJson, SyncEntityType.memoryV1: SyncMemoryV1.fromJson, SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson, SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson, @@ -172,8 +184,9 @@ const _kResponseMap = { SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson, SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson, + SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson, }; -class _SyncAckV1 { - static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1(); +class _SyncEmptyDto { + static _SyncEmptyDto? fromJson(dynamic _) => _SyncEmptyDto(); } diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 52ffaabca9..f4720fb110 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; @@ -29,9 +32,72 @@ class SyncStreamRepository extends DriftDatabaseRepository { SyncStreamRepository(super.db) : _db = db; + Future reset() async { + _logger.fine("SyncResetV1 received. Resetting remote entities"); + try { + await _db.exclusively(() async { + // foreign_keys PRAGMA is no-op within transactions + // https://www.sqlite.org/pragma.html#pragma_foreign_keys + await _db.customStatement('PRAGMA foreign_keys = OFF'); + await transaction(() async { + await _db.assetFaceEntity.deleteAll(); + await _db.memoryAssetEntity.deleteAll(); + await _db.memoryEntity.deleteAll(); + await _db.partnerEntity.deleteAll(); + await _db.personEntity.deleteAll(); + await _db.remoteAlbumAssetEntity.deleteAll(); + await _db.remoteAlbumEntity.deleteAll(); + await _db.remoteAlbumUserEntity.deleteAll(); + await _db.remoteAssetEntity.deleteAll(); + await _db.remoteExifEntity.deleteAll(); + await _db.stackEntity.deleteAll(); + await _db.userEntity.deleteAll(); + await _db.userMetadataEntity.deleteAll(); + }); + await _db.customStatement('PRAGMA foreign_keys = ON'); + }); + } catch (error, stack) { + _logger.severe('Error: SyncResetV1', error, stack); + rethrow; + } + } + + Future updateAuthUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final user in data) { + final companion = AuthUserEntityCompanion( + name: Value(user.name), + email: Value(user.email), + hasProfileImage: Value(user.hasProfileImage), + profileChangedAt: Value(user.profileChangedAt), + avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary), + isAdmin: Value(user.isAdmin), + pinCode: Value(user.pinCode), + quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0), + quotaUsageInBytes: Value(user.quotaUsageInBytes), + ); + + batch.insert( + _db.authUserEntity, + companion.copyWith(id: Value(user.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: SyncAuthUserV1', error, stack); + rethrow; + } + } + Future deleteUsersV1(Iterable data) async { try { - await _db.userEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId))); + await _db.batch((batch) { + for (final user in data) { + batch.deleteWhere(_db.userEntity, (row) => row.id.equals(user.userId)); + } + }); } catch (error, stack) { _logger.severe('Error: SyncUserDeleteV1', error, stack); rethrow; @@ -47,6 +113,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { email: Value(user.email), hasProfileImage: Value(user.hasProfileImage), profileChangedAt: Value(user.profileChangedAt), + avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary), ); batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion)); @@ -95,7 +162,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future deleteAssetsV1(Iterable data, {String debugLabel = 'user'}) async { try { - await _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId))); + await _db.batch((batch) { + for (final asset in data) { + batch.deleteWhere(_db.remoteAssetEntity, (row) => row.id.equals(asset.assetId)); + } + }); } catch (error, stack) { _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack); rethrow; @@ -180,7 +251,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future deleteAlbumsV1(Iterable data) async { try { - await _db.remoteAlbumEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId))); + await _db.batch((batch) { + for (final album in data) { + batch.deleteWhere(_db.remoteAlbumEntity, (row) => row.id.equals(album.albumId)); + } + }); } catch (error, stack) { _logger.severe('Error: deleteAlbumsV1', error, stack); rethrow; @@ -316,7 +391,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future deleteMemoriesV1(Iterable data) async { try { - await _db.memoryEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.memoryId))); + await _db.batch((batch) { + for (final memory in data) { + batch.deleteWhere(_db.memoryEntity, (row) => row.id.equals(memory.memoryId)); + } + }); } catch (error, stack) { _logger.severe('Error: deleteMemoriesV1', error, stack); rethrow; @@ -380,7 +459,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future deleteStacksV1(Iterable data, {String debugLabel = 'user'}) async { try { - await _db.stackEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.stackId))); + await _db.batch((batch) { + for (final stack in data) { + batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stack.stackId)); + } + }); } catch (error, stack) { _logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack); rethrow; @@ -573,3 +656,7 @@ extension on String { } } } + +extension on UserAvatarColor { + AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value); +} diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 61428ede92..05928d938f 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -42,14 +42,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository { throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket"); } - return _db.mergedAssetDrift - .mergedBucket(userIds: userIds, groupBy: groupBy.index) - .map((row) { - final date = row.bucketDate.dateFmt(groupBy); - return TimeBucket(date: date, assetCount: row.assetCount); - }) - .watch() - .throttle(const Duration(seconds: 3), trailing: true); + return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) { + final date = row.bucketDate.truncateDate(groupBy); + return TimeBucket(date: date, assetCount: row.assetCount); + }).watch(); } Future> _getMainBucketAssets(List userIds, {required int offset, required int count}) { @@ -127,7 +123,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(dateExp)]); return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); + final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; return TimeBucket(date: timeline, assetCount: assetCount); }).watch(); @@ -152,10 +148,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query.map((row) { - final asset = row.readTable(_db.localAssetEntity).toDto(); - return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id)); - }).get(); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto(remoteId: row.read(_db.remoteAssetEntity.id))) + .get(); } TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( @@ -169,17 +164,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .count(where: (row) => row.albumId.equals(albumId)) .map(_generateBuckets) .watch() - .map((results) => results.isNotEmpty ? results.first : []) - .handleError((error) { - return []; - }); + .map((results) => results.isNotEmpty ? results.first : const []) + .handleError((error) => const []); } return (_db.remoteAlbumEntity.select()..where((row) => row.id.equals(albumId))) .watch() .switchMap((albums) { if (albums.isEmpty) { - return Stream.value([]); + return Stream.value(const []); } final album = albums.first; @@ -206,15 +199,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); + final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; return TimeBucket(date: timeline, assetCount: assetCount); }).watch(); }) - .handleError((error) { - // If there's an error (e.g., album was deleted), return empty buckets - return []; - }); + // If there's an error (e.g., album was deleted), return empty buckets + .handleError((error) => const []); } Future> _getRemoteAlbumBucketAssets(String albumId, {required int offset, required int count}) async { @@ -222,17 +213,22 @@ class DriftTimelineRepository extends DriftDatabaseRepository { // If album doesn't exist (was deleted), return empty list if (albumData == null) { - return []; + return const []; } final isAscending = albumData.order == AlbumAssetOrder.asc; - final query = _db.remoteAssetEntity.select().join([ + final query = _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([ innerJoin( _db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id), useColumns: false, ), + leftOuterJoin( + _db.localAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + useColumns: false, + ), ])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId)); if (isAscending) { @@ -243,12 +239,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.limit(count, offset: offset); - return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id))) + .get(); } TimelineQuery fromAssets(List assets) => ( bucketSource: () => Stream.value(_generateBuckets(assets.length)), - assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList()), + assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList(growable: false)), ); TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder( @@ -330,7 +328,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(dateExp)]); return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); + final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; return TimeBucket(date: timeline, assetCount: assetCount); }).watch(); @@ -401,7 +399,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(dateExp)]); return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); + final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; return TimeBucket(date: timeline, assetCount: assetCount); }).watch(); @@ -433,12 +431,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); } - TimelineQuery map(LatLngBounds bounds, GroupAssetsBy groupBy) => ( - bucketSource: () => _watchMapBucket(bounds, groupBy: groupBy), - assetSource: (offset, count) => _getMapBucketAssets(bounds, offset: offset, count: count), + TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy), + assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count), ); - Stream> _watchMapBucket(LatLngBounds bounds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { + Stream> _watchMapBucket( + String userId, + LatLngBounds bounds, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { if (groupBy == GroupAssetsBy.none) { // TODO: Support GroupAssetsBy.none throw UnsupportedError("GroupAssetsBy.none is not supported for _watchMapBucket"); @@ -457,7 +459,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), ]) ..where( - _db.remoteExifEntity.inBounds(bounds) & + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteExifEntity.inBounds(bounds) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.deletedAt.isNull(), ) @@ -465,13 +468,18 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(dateExp)]); return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); + final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; return TimeBucket(date: timeline, assetCount: assetCount); }).watch(); } - Future> _getMapBucketAssets(LatLngBounds bounds, {required int offset, required int count}) { + Future> _getMapBucketAssets( + String userId, + LatLngBounds bounds, { + required int offset, + required int count, + }) { final query = _db.remoteAssetEntity.select().join([ innerJoin( @@ -481,7 +489,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), ]) ..where( - _db.remoteExifEntity.inBounds(bounds) & + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteExifEntity.inBounds(bounds) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.deletedAt.isNull(), ) @@ -490,6 +499,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); } + @pragma('vm:prefer-inline') TimelineQuery _remoteQueryBuilder({ required Expression Function($RemoteAssetEntityTable row) filter, GroupAssetsBy groupBy = GroupAssetsBy.day, @@ -521,12 +531,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(dateExp)]); return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); + final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; return TimeBucket(date: timeline, assetCount: assetCount); }).watch(); } + @pragma('vm:prefer-inline') Future> _getRemoteAssets({ required Expression Function($RemoteAssetEntityTable row) filter, required int offset, @@ -547,11 +558,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query.map((row) { - final asset = row.readTable(_db.remoteAssetEntity).toDto(); - final localId = row.read(_db.localAssetEntity.id); - return asset.copyWith(localId: localId); - }).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id))) + .get(); } else { final query = _db.remoteAssetEntity.select() ..where(filter) @@ -564,12 +573,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } List _generateBuckets(int count) { - final buckets = List.generate( - (count / kTimelineNoneSegmentSize).floor(), - (_) => const Bucket(assetCount: kTimelineNoneSegmentSize), + final buckets = List.filled( + (count / kTimelineNoneSegmentSize).ceil(), + const Bucket(assetCount: kTimelineNoneSegmentSize), ); if (count % kTimelineNoneSegmentSize != 0) { - buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize)); + buckets[buckets.length - 1] = Bucket(assetCount: count % kTimelineNoneSegmentSize); } return buckets; } @@ -588,16 +597,12 @@ extension on Expression { } extension on String { - DateTime dateFmt(GroupAssetsBy groupBy) { + DateTime truncateDate(GroupAssetsBy groupBy) { final format = switch (groupBy) { GroupAssetsBy.day || GroupAssetsBy.auto => "y-M-d", GroupAssetsBy.month => "y-M", GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"), }; - try { - return DateFormat(format).parse(this); - } catch (e) { - throw FormatException("Invalid date format: $this", e); - } + return DateFormat(format, 'en').parse(this); } } diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index 3081aee1a9..d4eb1ceed6 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -2,8 +2,8 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; import 'package:isar/isar.dart'; @@ -68,12 +68,12 @@ class IsarUserRepository extends IsarDatabaseRepository { } } -class DriftUserRepository extends DriftDatabaseRepository { +class DriftAuthUserRepository extends DriftDatabaseRepository { final Drift _db; - const DriftUserRepository(super.db) : _db = db; + const DriftAuthUserRepository(super.db) : _db = db; Future get(String id) async { - final user = await _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull(); + final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull(); if (user == null) return null; @@ -84,43 +84,30 @@ class DriftUserRepository extends DriftDatabaseRepository { } Future upsert(UserDto user) async { - await _db.userEntity.insertOnConflictUpdate( - UserEntityCompanion( + await _db.authUserEntity.insertOnConflictUpdate( + AuthUserEntityCompanion( id: Value(user.id), - isAdmin: Value(user.isAdmin), - updatedAt: Value(user.updatedAt), name: Value(user.name), email: Value(user.email), hasProfileImage: Value(user.hasProfileImage), profileChangedAt: Value(user.profileChangedAt), + isAdmin: Value(user.isAdmin), + quotaSizeInBytes: Value(user.quotaSizeInBytes), + quotaUsageInBytes: Value(user.quotaUsageInBytes), + avatarColor: Value(user.avatarColor), ), ); return user; } - - Future> getAll() async { - final users = await _db.userEntity.select().get(); - final List result = []; - - for (final user in users) { - final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id)); - final metadata = await query.map((row) => row.toDto()).get(); - result.add(user.toDto(metadata)); - } - - return result; - } } -extension on UserEntityData { +extension on AuthUserEntityData { UserDto toDto([List? metadata]) { - AvatarColor avatarColor = AvatarColor.primary; bool memoryEnabled = true; if (metadata != null) { for (final meta in metadata) { if (meta.key == UserMetadataKey.preferences && meta.preferences != null) { - avatarColor = meta.preferences?.userAvatarColor ?? AvatarColor.primary; memoryEnabled = meta.preferences?.memoriesEnabled ?? true; } } @@ -130,12 +117,13 @@ extension on UserEntityData { id: id, email: email, name: name, - isAdmin: isAdmin, - updatedAt: updatedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, avatarColor: avatarColor, memoryEnabled: memoryEnabled, + isAdmin: isAdmin, + quotaSizeInBytes: quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes, ); } } diff --git a/mobile/lib/infrastructure/utils/user.converter.dart b/mobile/lib/infrastructure/utils/user.converter.dart index dc107e6fb2..826649b247 100644 --- a/mobile/lib/infrastructure/utils/user.converter.dart +++ b/mobile/lib/infrastructure/utils/user.converter.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:openapi/api.dart'; // TODO: Move to repository once all classes are refactored @@ -29,6 +28,8 @@ abstract final class UserConverter { isPartnerSharedWith: false, profileChangedAt: adminDto.profileChangedAt, hasProfileImage: adminDto.profileImagePath.isNotEmpty, + quotaSizeInBytes: adminDto.quotaSizeInBytes ?? 0, + quotaUsageInBytes: adminDto.quotaUsageInBytes ?? 0, ); static UserDto fromPartnerDto(PartnerResponseDto dto) => UserDto( diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 0cab21748c..712ee0bd83 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -12,12 +12,16 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; +import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/db.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/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; @@ -30,6 +34,7 @@ import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/licenses.dart'; import 'package:immich_mobile/utils/migration.dart'; @@ -40,6 +45,7 @@ import 'package:worker_manager/worker_manager.dart'; void main() async { ImmichWidgetsBinding(); + unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); final (isar, drift, logDb) = await Bootstrap.initDB(); await Bootstrap.initDomain(isar, drift, logDb); await initApp(); @@ -67,9 +73,9 @@ Future initApp() async { if (kReleaseMode && Platform.isAndroid) { try { await FlutterDisplayMode.setHighRefreshRate(); - debugPrint("Enabled high refresh mode"); + dPrint(() => "Enabled high refresh mode"); } catch (e) { - debugPrint("Error setting high refresh rate: $e"); + dPrint(() => "Error setting high refresh rate: $e"); } } @@ -124,23 +130,23 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: - debugPrint("[APP STATE] resumed"); + dPrint(() => "[APP STATE] resumed"); ref.read(appStateProvider.notifier).handleAppResume(); break; case AppLifecycleState.inactive: - debugPrint("[APP STATE] inactive"); + dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); break; case AppLifecycleState.paused: - debugPrint("[APP STATE] paused"); + dPrint(() => "[APP STATE] paused"); ref.read(appStateProvider.notifier).handleAppPause(); break; case AppLifecycleState.detached: - debugPrint("[APP STATE] detached"); + dPrint(() => "[APP STATE] detached"); ref.read(appStateProvider.notifier).handleAppDetached(); break; case AppLifecycleState.hidden: - debugPrint("[APP STATE] hidden"); + dPrint(() => "[APP STATE] hidden"); ref.read(appStateProvider.notifier).handleAppHidden(); break; } @@ -165,36 +171,6 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve await ref.read(localNotificationService).setup(); } - void _configureFileDownloaderNotifications() { - FileDownloader().configureNotificationForGroup( - kDownloadGroupImage, - running: TaskNotification('downloading_media'.tr(), '${'file_name'.tr()}: {filename}'), - complete: TaskNotification('download_finished'.tr(), '${'file_name'.tr()}: {filename}'), - progressBar: true, - ); - - FileDownloader().configureNotificationForGroup( - kDownloadGroupVideo, - running: TaskNotification('downloading_media'.tr(), '${'file_name'.tr()}: {filename}'), - complete: TaskNotification('download_finished'.tr(), '${'file_name'.tr()}: {filename}'), - progressBar: true, - ); - - FileDownloader().configureNotificationForGroup( - kManualUploadGroup, - running: TaskNotification('uploading_media'.tr(), '${'file_name'.tr()}: {displayName}'), - complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'), - progressBar: true, - ); - - FileDownloader().configureNotificationForGroup( - kBackupGroup, - running: TaskNotification('uploading_media'.tr(), '${'file_name'.tr()}: {displayName}'), - complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'), - progressBar: true, - ); - } - Future _deepLinkBuilder(PlatformDeepLink deepLink) async { final deepLinkHandler = ref.read(deepLinkServiceProvider); final currentRouteName = ref.read(currentRouteNameProvider.notifier).state; @@ -202,13 +178,13 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name; if (deepLink.uri.scheme == "immich") { - final proposedRoute = await deepLinkHandler.handleScheme(deepLink, isColdStart); + final proposedRoute = await deepLinkHandler.handleScheme(deepLink, ref, isColdStart); return proposedRoute; } if (deepLink.uri.host == "my.immich.app") { - final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, isColdStart); + final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, ref, isColdStart); return proposedRoute; } @@ -221,17 +197,23 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve super.didChangeDependencies(); Intl.defaultLocale = context.locale.toLanguageTag(); WidgetsBinding.instance.addPostFrameCallback((_) { - _configureFileDownloaderNotifications(); + configureFileDownloaderNotifications(); }); } @override initState() { super.initState(); - initApp().then((_) => debugPrint("App Init Completed")); + initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + if (Store.isBetaTimelineEnabled) { + ref.read(backgroundServiceProvider).disableService(); + ref.read(backgroundWorkerFgServiceProvider).enable(); + } else { + ref.read(backgroundWorkerFgServiceProvider).disable(); + ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + } }); ref.read(shareIntentUploadProvider.notifier).init(); @@ -261,7 +243,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, - navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()], + navigatorObservers: () => [AppNavigationObserver(ref: ref)], ), ), ); diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart index e659340f26..20d4dbd325 100644 --- a/mobile/lib/pages/album/album_options.page.dart +++ b/mobile/lib/pages/album/album_options.page.dart @@ -169,7 +169,7 @@ class AlbumOptionsPage extends HookConsumerWidget { album.activityEnabled = value; } }, - activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, + activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, dense: true, title: Text( "comments_and_likes", diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 093ff952ae..4f55d00ea0 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -205,9 +205,9 @@ class BackupControllerPage extends HookConsumerWidget { } buildBackgroundBackupInfo() { - return const ListTile( - leading: Icon(Icons.info_outline_rounded), - title: Text("Background backup is currently running, cannot start manual backup"), + return ListTile( + leading: const Icon(Icons.info_outline_rounded), + title: Text('background_backup_running_error'.tr()), ); } diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index b125c35908..5a2cab8dd6 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -1,18 +1,26 @@ +import 'dart:async'; + 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/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; +import 'package:logging/logging.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() class DriftBackupPage extends ConsumerStatefulWidget { @@ -23,30 +31,39 @@ class DriftBackupPage extends ConsumerStatefulWidget { } class _DriftBackupPageState extends ConsumerState { + bool? syncSuccess; + @override void initState() { super.initState(); + + WakelockPlus.enable(); + final currentUser = ref.read(currentUserProvider); if (currentUser == null) { return; } - ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + + ref.read(driftBackupProvider.notifier).updateSyncing(true); + syncSuccess = await ref.read(backgroundSyncProvider).syncRemote(); + ref + .read(driftBackupProvider.notifier) + .updateError(syncSuccess == true ? BackupError.none : BackupError.syncFailed); + ref.read(driftBackupProvider.notifier).updateSyncing(false); + + if (mounted) { + await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + } + }); } - Future startBackup() async { - final currentUser = ref.read(currentUserProvider); - if (currentUser == null) { - return; - } - - await ref.read(backgroundSyncProvider).syncRemote(); - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); - await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id); - } - - Future stopBackup() async { - await ref.read(driftBackupProvider.notifier).cancel(); + @override + dispose() { + super.dispose(); + WakelockPlus.disable(); } @override @@ -56,6 +73,37 @@ class _DriftBackupPageState extends ConsumerState { .where((album) => album.backupSelection == BackupSelection.selected) .toList(); + final error = ref.watch(driftBackupProvider.select((p) => p.error)); + + final backupNotifier = ref.read(driftBackupProvider.notifier); + final backupSyncManager = ref.read(backgroundSyncProvider); + + Future startBackup() async { + final currentUser = Store.tryGet(StoreKey.currentUser); + if (currentUser == null) { + return; + } + + if (syncSuccess == null) { + ref.read(driftBackupProvider.notifier).updateSyncing(true); + syncSuccess = await backupSyncManager.syncRemote(); + ref.read(driftBackupProvider.notifier).updateSyncing(false); + } + + await backupNotifier.getBackupStatus(currentUser.id); + + if (syncSuccess == false) { + Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup"); + backupNotifier.updateError(BackupError.syncFailed); + return; + } + await backupNotifier.startBackup(currentUser.id); + } + + Future stopBackup() async { + await backupNotifier.cancel(); + } + return Scaffold( appBar: AppBar( elevation: 0, @@ -90,7 +138,33 @@ class _DriftBackupPageState extends ConsumerState { const _BackupCard(), const _RemainderCard(), const Divider(), - BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()), + BackupToggleButton( + onStart: () async => await startBackup(), + onStop: () async { + syncSuccess = null; + await stopBackup(); + }, + ), + switch (error) { + BackupError.none => const SizedBox.shrink(), + BackupError.syncFailed => Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Icon(Icons.warning_rounded, color: context.colorScheme.error, fill: 1), + const SizedBox(width: 8), + Text( + IntlKeys.backup_error_sync_failed.t(), + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), + textAlign: TextAlign.center, + ), + ], + ), + ), + }, TextButton.icon( icon: const Icon(Icons.info_outline_rounded), onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), @@ -230,11 +304,13 @@ class _BackupCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final backupCount = ref.watch(driftBackupProvider.select((p) => p.backupCount)); + final syncStatus = ref.watch(syncStatusProvider); return BackupInfoCard( title: "backup_controller_page_backup".tr(), subtitle: "backup_controller_page_backup_sub".tr(), info: backupCount.toString(), + isLoading: syncStatus.isRemoteSyncing, ); } } @@ -245,11 +321,207 @@ class _RemainderCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount)); - return BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: remainderCount.toString(), - onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()), + final syncStatus = ref.watch(syncStatusProvider); + + return Card( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), + side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), + ), + elevation: 0, + borderOnForeground: false, + child: Column( + children: [ + ListTile( + minVerticalPadding: 18, + isThreeLine: true, + title: Text("backup_controller_page_remainder".t(context: context), style: context.textTheme.titleMedium), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4.0, right: 18.0), + child: Text( + "backup_controller_page_remainder_sub".t(context: context), + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + children: [ + Text( + remainderCount.toString(), + style: context.textTheme.titleLarge?.copyWith( + color: context.colorScheme.onSurface.withAlpha(syncStatus.isRemoteSyncing ? 50 : 255), + ), + ), + if (syncStatus.isRemoteSyncing) + Positioned.fill( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: context.colorScheme.onSurface.withAlpha(150), + ), + ), + ), + ), + ], + ), + Text( + "backup_info_card_assets", + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurface.withAlpha(syncStatus.isRemoteSyncing ? 50 : 255), + ), + ).tr(), + ], + ), + ), + const Divider(height: 0), + const _PreparingStatus(), + const Divider(height: 0), + + ListTile( + enableFeedback: true, + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 0.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()), + title: Text( + "view_details".t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + ), + trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant), + ), + ], + ), + ); + } +} + +class _PreparingStatus extends ConsumerStatefulWidget { + const _PreparingStatus(); + + @override + _PreparingStatusState createState() => _PreparingStatusState(); +} + +class _PreparingStatusState extends ConsumerState { + Timer? _pollingTimer; + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } + + void _startPollingIfNeeded() { + if (_pollingTimer != null) return; + + _pollingTimer = Timer.periodic(const Duration(seconds: 3), (timer) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser != null && mounted) { + await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + + // Stop polling if processing count reaches 0 + final updatedProcessingCount = ref.read(driftBackupProvider.select((p) => p.processingCount)); + if (updatedProcessingCount == 0) { + timer.cancel(); + _pollingTimer = null; + } + } else { + timer.cancel(); + _pollingTimer = null; + } + }); + } + + @override + Widget build(BuildContext context) { + final syncStatus = ref.watch(syncStatusProvider); + final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount)); + final processingCount = ref.watch(driftBackupProvider.select((p) => p.processingCount)); + final readyForUploadCount = remainderCount - processingCount; + + ref.listen(driftBackupProvider.select((p) => p.processingCount), (previous, next) { + if (next > 0 && _pollingTimer == null) { + _startPollingIfNeeded(); + } else if (next == 0 && _pollingTimer != null) { + _pollingTimer?.cancel(); + _pollingTimer = null; + } + }); + + if (!syncStatus.isHashing) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 1.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerHigh.withValues(alpha: 0.5), + shape: BoxShape.rectangle, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "preparing".t(context: context), + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurface.withAlpha(200), + ), + ), + const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 1.5)), + ], + ), + const SizedBox(height: 2), + Text( + processingCount.toString(), + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + decoration: BoxDecoration(color: context.colorScheme.primary.withValues(alpha: 0.1)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "ready_for_upload".t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + ), + const SizedBox(height: 2), + Text( + readyForUploadCount.toString(), + style: context.textTheme.titleMedium?.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], ); } } diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index 865845525a..d49f71ce52 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -1,18 +1,24 @@ +import 'dart:async'; import 'dart:io'; 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/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; +import 'package:logging/logging.dart'; @RoutePage() class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget { @@ -26,10 +32,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState _enableSyncUploadAlbum; late TextEditingController _searchController; late FocusNode _searchFocusNode; + Future? _handleLinkedAlbumFuture; @override void initState() { @@ -44,6 +50,26 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); } + Future _handlePagePopped() async { + final user = ref.read(currentUserProvider); + if (user == null) { + return; + } + + final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); + final selectedAlbums = ref + .read(backupAlbumProvider) + .where((a) => a.backupSelection == BackupSelection.selected) + .toList(); + + if (enableSyncUploadAlbum && selectedAlbums.isNotEmpty) { + setState(() { + _handleLinkedAlbumFuture = ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedAlbums, user.id); + }); + await _handleLinkedAlbumFuture; + } + } + @override void dispose() { _enableSyncUploadAlbum.dispose(); @@ -65,42 +91,44 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState album.backupSelection == BackupSelection.selected).toList(); final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList(); - // handleSyncAlbumToggle(bool isEnable) async { - // if (isEnable) { - // await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - // for (final album in selectedBackupAlbums) { - // await ref.read(albumProvider.notifier).createSyncAlbum(album.name); - // } - // } - // } - return PopScope( - onPopInvokedWithResult: (didPop, result) async { - // There is an issue with Flutter where the pop event - // can be triggered multiple times, so we guard it with _hasPopped - if (didPop && !_hasPopped) { - _hasPopped = true; + canPop: false, + onPopInvokedWithResult: (didPop, _) async { + if (!didPop) { + await _handlePagePopped(); - final currentUser = ref.read(currentUserProvider); - if (currentUser == null) { + final user = ref.read(currentUserProvider); + if (user == null) { return; } - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id); final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); - - if (currentTotalAssetCount != _initialTotalAssetCount) { - final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); - - if (!isBackupEnabled) { - return; + final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; + final backupNotifier = ref.read(driftBackupProvider.notifier); + final backgroundSync = ref.read(backgroundSyncProvider); + final nativeSync = ref.read(nativeSyncApiProvider); + if (totalChanged) { + // Waits for hashing to be cancelled before starting a new one + unawaited(nativeSync.cancelHashing().whenComplete(() => backgroundSync.hashAssets())); + if (isBackupEnabled) { + unawaited( + backupNotifier.cancel().whenComplete( + () => backgroundSync.syncRemote().then((success) { + if (success) { + return backupNotifier.startBackup(user.id); + } else { + Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup'); + backupNotifier.updateError(BackupError.syncFailed); + } + }), + ), + ); } - final backupNotifier = ref.read(driftBackupProvider.notifier); - - backupNotifier.cancel().then((_) { - backupNotifier.startBackup(currentUser.id); - }); } + + Navigator.of(context).pop(); } }, child: Scaffold( @@ -139,103 +167,123 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState 600) { + return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery); + } else { + return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery); + } + }, + ), + ], + ), + if (_handleLinkedAlbumFuture != null) + FutureBuilder( + future: _handleLinkedAlbumFuture, + builder: (context, snapshot) { + return SizedBox( + height: double.infinity, + width: double.infinity, + child: Container( + color: context.scaffoldBackgroundColor.withValues(alpha: 0.8), + child: Center( + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + const CircularProgressIndicator(strokeWidth: 4), + Text('creating_linked_albums'.tr(), style: context.textTheme.labelLarge), + ], + ), + ), + ), + ); + }, ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery); - } else { - return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery); - } - }, - ), ], ), ), diff --git a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart index d14d925c3d..3cc675c4ad 100644 --- a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart +++ b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -58,8 +59,10 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { overflow: TextOverflow.ellipsis, ); }, - error: (error, stackTrace) => - Text('Error: $error', style: TextStyle(color: context.colorScheme.error)), + error: (error, stackTrace) => Text( + 'error_saving_image'.tr(args: [error.toString()]), + style: TextStyle(color: context.colorScheme.error), + ), loading: () => const SizedBox(height: 16, width: 16, child: CircularProgressIndicator.adaptive()), ), ], @@ -83,7 +86,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { ); }, error: (Object error, StackTrace stackTrace) { - return Center(child: Text('Error: $error')); + return Center(child: Text('error_saving_image'.tr(args: [error.toString()]))); }, loading: () { return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive())); diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 92f911ae1e..f18dc48dca 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -5,10 +7,12 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; +import 'package:logging/logging.dart'; @RoutePage() class DriftBackupOptionsPage extends ConsumerWidget { @@ -54,9 +58,19 @@ class DriftBackupOptionsPage extends ConsumerWidget { ); final backupNotifier = ref.read(driftBackupProvider.notifier); - backupNotifier.cancel().then((_) { - backupNotifier.startBackup(currentUser.id); - }); + final backgroundSync = ref.read(backgroundSyncProvider); + unawaited( + backupNotifier.cancel().whenComplete( + () => backgroundSync.syncRemote().then((success) { + if (success) { + return backupNotifier.startBackup(currentUser.id); + } else { + Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup'); + backupNotifier.updateError(BackupError.syncFailed); + } + }), + ), + ); } }, child: Scaffold( diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index bececddc7f..80956b708f 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -1,12 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:path/path.dart' as path; @@ -82,6 +82,7 @@ class DriftUploadDetailPage extends ConsumerWidget { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, children: [ Text( path.basename(item.filename), @@ -89,7 +90,13 @@ class DriftUploadDetailPage extends ConsumerWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 4), + if (item.error != null) + Text( + item.error!, + style: context.textTheme.bodySmall?.copyWith( + color: context.colorScheme.onErrorContainer.withValues(alpha: 0.6), + ), + ), Text( 'Tap for more details', style: context.textTheme.bodySmall?.copyWith( diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index fe0c0ea442..37aec2f13c 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,7 +9,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; -import 'package:intl/intl.dart'; @RoutePage() class AppLogPage extends HookConsumerWidget { @@ -49,7 +49,7 @@ class AppLogPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text("Logs", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), + title: Text('logs'.tr(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), scrolledUnderElevation: 1, elevation: 2, actions: [ diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index c9773f36e1..de9604b7ad 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -36,7 +37,7 @@ class AppLogDetailPage extends HookConsumerWidget { context.scaffoldMessenger.showSnackBar( SnackBar( content: Text( - "Copied to clipboard", + "copied_to_clipboard".tr(), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -97,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget { } return Scaffold( - appBar: AppBar(title: const Text("Log Detail")), + appBar: AppBar(title: Text("log_detail_title".tr())), body: SafeArea( child: ListView( children: [ diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart index 3e9747ce32..2cc3dede1e 100644 --- a/mobile/lib/pages/common/change_experience.page.dart +++ b/mobile/lib/pages/common/change_experience.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -13,7 +15,11 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.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/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/utils/migration.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -39,45 +45,13 @@ class _ChangeExperiencePageState extends ConsumerState { Future _handleMigration() async { try { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await _performMigrationLogic().timeout( + const Duration(minutes: 3), + onTimeout: () async { + await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + }, + ); if (mounted) { setState(() { @@ -95,6 +69,58 @@ class _ChangeExperiencePageState extends ConsumerState { } } + Future _performMigrationLogic() async { + if (widget.switchingToBeta) { + final assetNotifier = ref.read(assetProvider.notifier); + if (assetNotifier.mounted) { + assetNotifier.dispose(); + } + final albumNotifier = ref.read(albumProvider.notifier); + if (albumNotifier.mounted) { + albumNotifier.dispose(); + } + + // Cancel uploads + await Store.put(StoreKey.backgroundBackup, false); + ref + .read(backupProvider.notifier) + .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); + ref.read(backupProvider.notifier).setAutoBackup(false); + ref.read(backupProvider.notifier).cancelBackup(); + ref.read(manualUploadProvider.notifier).cancelBackup(); + // Start listening to new websocket events + ref.read(websocketProvider.notifier).stopListenToOldEvents(); + ref.read(websocketProvider.notifier).startListeningToBetaEvents(); + + await ref.read(driftProvider).reset(); + await Store.put(StoreKey.shouldResetSync, true); + final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); + if (delay >= 1000) { + await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); + } + final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + + if (permission.isGranted) { + await ref.read(backgroundSyncProvider).syncLocal(full: true); + await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await ref.read(backgroundServiceProvider).disableService(); + } + } else { + await ref.read(backgroundSyncProvider).cancel(); + ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); + ref.read(websocketProvider.notifier).startListeningToOldEvents(); + ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); + await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); + await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + await ref.read(backgroundWorkerFgServiceProvider).disable(); + } + + await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 7bc8cd2b3a..b23c420971 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -11,8 +11,7 @@ import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_se import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; -import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; +import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart'; @@ -20,7 +19,6 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se import 'package:immich_mobile/widgets/settings/settings_card.dart'; enum SettingSection { - beta('beta_sync', Icons.sync_outlined, "beta_sync_subtitle"), advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), @@ -28,14 +26,14 @@ enum SettingSection { networking('networking_settings', Icons.wifi, "networking_subtitle"), notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"), preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"), - timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"); + timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"), + beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"); final String title; final String subtitle; final IconData icon; Widget get widget => switch (this) { - SettingSection.beta => const _BetaLandscapeToggle(), SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), SettingSection.backup => @@ -45,6 +43,7 @@ enum SettingSection { SettingSection.notifications => const NotificationSetting(), SettingSection.preferences => const PreferenceSetting(), SettingSection.timeline => const AssetListSettings(), + SettingSection.beta => const SyncStatusAndActions(), }; const SettingSection(this.title, this.icon, this.subtitle); @@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget { context.locale; return Scaffold( appBar: AppBar(centerTitle: false, title: const Text('settings').tr()), - body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), + body: context.isMobile ? const SafeArea(child: _MobileLayout()) : const SafeArea(child: _TabletLayout()), ); } } @@ -72,13 +71,12 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - const BetaTimelineListTile(), if (Store.isBetaTimelineEnabled) SettingsCard( icon: Icons.sync_outlined, - title: 'beta_sync'.tr(), - subtitle: 'beta_sync_subtitle'.tr(), - settingRoute: const BetaSyncSettingsRoute(), + title: 'sync_status'.tr(), + subtitle: 'sync_status_subtitle'.tr(), + settingRoute: const SyncStatusRoute(), ), ] : [ @@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget { .toList(); return ListView( physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.only(top: 10.0, bottom: 56), + padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings], ); } @@ -134,21 +132,6 @@ class _TabletLayout extends HookWidget { } } -class _BetaLandscapeToggle extends HookWidget { - const _BetaLandscapeToggle(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(height: 100, child: BetaTimelineListTile()), - if (Store.isBetaTimelineEnabled) const Expanded(child: BetaSyncSettings()), - ], - ); - } -} - @RoutePage() class SettingsSubPage extends StatelessWidget { const SettingsSubPage(this.section, {super.key}); @@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget { @override Widget build(BuildContext context) { context.locale; - return Scaffold( - appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), - body: section.widget, + return SafeArea( + bottom: true, + top: false, + right: true, + child: Scaffold( + appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), + body: section.widget, + ), ); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 87ea7849c6..5147ba8f45 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -1,10 +1,14 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; @@ -21,6 +25,7 @@ class SplashScreenPage extends StatefulHookConsumerWidget { class SplashScreenPageState extends ConsumerState { final log = Logger("SplashScreenPage"); + @override void initState() { super.initState(); @@ -47,11 +52,38 @@ class SplashScreenPageState extends ConsumerState { if (accessToken != null && serverUrl != null && endpoint != null) { final infoProvider = ref.read(serverInfoProvider.notifier); final wsProvider = ref.read(websocketProvider.notifier); + final backgroundManager = ref.read(backgroundSyncProvider); + final backupProvider = ref.read(driftBackupProvider.notifier); + ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( - (a) { + (_) async { try { wsProvider.connect(); infoProvider.getServerInfo(); + + if (Store.isBetaTimelineEnabled) { + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); + + if (syncSuccess) { + await Future.wait([ + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + ]); + } else { + backupProvider.updateError(BackupError.syncFailed); + await backgroundManager.hashAssets(); + } + + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); + } + } } catch (e) { log.severe('Failed establishing connection to the server: $e'); } @@ -69,7 +101,16 @@ class SplashScreenPageState extends ConsumerState { return; } + // clean install - change the default of the flag + // current install not using beta timeline if (context.router.current.name == SplashScreenRoute.name) { + final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); + if (needBetaMigration) { + await Store.put(StoreKey.needBetaMigration, false); + context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]); + return; + } + context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); } @@ -84,6 +125,17 @@ class SplashScreenPageState extends ConsumerState { } } + Future _resumeBackup(DriftBackupNotifier notifier) async { + final isEnableBackup = Store.get(StoreKey.enableBackup, false); + + if (isEnableBackup) { + final currentUser = Store.tryGet(StoreKey.currentUser); + if (currentUser != null) { + notifier.handleBackupResume(currentUser.id); + } + } + } + @override Widget build(BuildContext context) { return const Scaffold( diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index 983164831a..b60fe1ddc1 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -7,18 +7,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.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'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; @RoutePage() class TabShellPage extends ConsumerStatefulWidget { @@ -29,31 +26,10 @@ class TabShellPage extends ConsumerStatefulWidget { } class _TabShellPageState extends ConsumerState { - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - ref.read(websocketProvider.notifier).connect(); - - final isEnableBackup = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); - - await runNewSync(ref, full: true).then((_) async { - if (isEnableBackup) { - final currentUser = ref.read(currentUserProvider); - if (currentUser == null) { - return; - } - - await ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); - } - }); - }); - } - @override Widget build(BuildContext context) { final isScreenLandscape = context.orientation == Orientation.landscape; + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final navigationDestinations = [ NavigationDestination( @@ -65,23 +41,33 @@ class _TabShellPageState extends ConsumerState { label: 'search'.tr(), icon: const Icon(Icons.search_rounded), selectedIcon: Icon(Icons.search, color: context.primaryColor), + enabled: !isReadonlyModeEnabled, ), NavigationDestination( label: 'albums'.tr(), icon: const Icon(Icons.photo_album_outlined), selectedIcon: Icon(Icons.photo_album_rounded, color: context.primaryColor), + enabled: !isReadonlyModeEnabled, ), NavigationDestination( label: 'library'.tr(), icon: const Icon(Icons.space_dashboard_outlined), selectedIcon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), + enabled: !isReadonlyModeEnabled, ), ]; Widget navigationRail(TabsRouter tabsRouter) { return NavigationRail( destinations: navigationDestinations - .map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon)) + .map( + (e) => NavigationRailDestination( + icon: e.icon, + label: Text(e.label), + selectedIcon: e.selectedIcon, + disabled: !e.enabled, + ), + ) .toList(), onDestinationSelected: (index) => _onNavigationSelected(tabsRouter, index, ref), selectedIndex: tabsRouter.activeIndex, @@ -124,6 +110,10 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { EventStream.shared.emit(const ScrollToTopEvent()); } + if (index == 0) { + ref.invalidate(driftMemoryFutureProvider); + } + // On Search page tapped if (router.activeIndex == 1 && index == 1) { ref.read(searchInputFocusProvider).requestFocus(); @@ -134,8 +124,10 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { ref.read(remoteAlbumProvider.notifier).refresh(); } + // Library page if (index == 3) { ref.invalidate(localAlbumProvider); + ref.invalidate(driftGetAllPeopleProvider); } ref.read(hapticFeedbackProvider.notifier).selectionClick(); diff --git a/mobile/lib/pages/library/partner/drift_partner.page.dart b/mobile/lib/pages/library/partner/drift_partner.page.dart index 834e55ffd4..d81cc44c76 100644 --- a/mobile/lib/pages/library/partner/drift_partner.page.dart +++ b/mobile/lib/pages/library/partner/drift_partner.page.dart @@ -134,7 +134,7 @@ class _SharedToPartnerList extends ConsumerWidget { ); }, loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center(child: Text("Error loading partners: $error")), + error: (error, stack) => Center(child: Text('error_loading_partners'.tr(args: [error.toString()]))), ); } } diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index 73c38a109c..f376709316 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -85,7 +85,7 @@ class PlacesCollectionPage extends HookConsumerWidget { }, ); }, - error: (error, stask) => const Text('Error getting places'), + error: (error, stask) => Text('error_getting_places'.tr()), loading: () => const Center(child: CircularProgressIndicator()), ), ], diff --git a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart index c78a9d5138..8b66bb231f 100644 --- a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart @@ -112,7 +112,7 @@ class SharedLinkEditPage extends HookConsumerWidget { return SwitchListTile.adaptive( value: showMetadata.value, onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null, - activeColor: colorScheme.primary, + activeThumbColor: colorScheme.primary, dense: true, title: Text("show_metadata", style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(), ); @@ -122,7 +122,7 @@ class SharedLinkEditPage extends HookConsumerWidget { return SwitchListTile.adaptive( value: allowDownload.value, onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null, - activeColor: colorScheme.primary, + activeThumbColor: colorScheme.primary, dense: true, title: Text( "allow_public_user_to_download", @@ -135,7 +135,7 @@ class SharedLinkEditPage extends HookConsumerWidget { return SwitchListTile.adaptive( value: allowUpload.value, onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null, - activeColor: colorScheme.primary, + activeThumbColor: colorScheme.primary, dense: true, title: Text( "allow_public_user_to_upload", @@ -148,7 +148,7 @@ class SharedLinkEditPage extends HookConsumerWidget { return SwitchListTile.adaptive( value: editExpiry.value, onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null, - activeColor: colorScheme.primary, + activeThumbColor: colorScheme.primary, dense: true, title: Text( "change_expiration_time", diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 97205e000c..acfa2fd59f 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -435,7 +435,7 @@ class SearchPage extends HookConsumerWidget { } }, icon: const Icon(Icons.more_vert_rounded), - tooltip: 'Show text search menu', + tooltip: 'show_text_search_menu'.tr(), ); }, menuChildren: [ diff --git a/mobile/lib/pages/settings/beta_sync_settings.page.dart b/mobile/lib/pages/settings/sync_status.page.dart similarity index 71% rename from mobile/lib/pages/settings/beta_sync_settings.page.dart rename to mobile/lib/pages/settings/sync_status.page.dart index 992557b7c6..d54ba89e5d 100644 --- a/mobile/lib/pages/settings/beta_sync_settings.page.dart +++ b/mobile/lib/pages/settings/sync_status.page.dart @@ -1,25 +1,25 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart'; +import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; @RoutePage() -class BetaSyncSettingsPage extends StatelessWidget { - const BetaSyncSettingsPage({super.key}); +class SyncStatusPage extends StatelessWidget { + const SyncStatusPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, - title: const Text("beta_sync").t(context: context), + title: const Text("sync_status").t(context: context), leading: IconButton( onPressed: () => context.maybePop(true), splashRadius: 24, icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: const BetaSyncSettings(), + body: const SyncStatusAndActions(), ); } } diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart new file mode 100644 index 0000000000..af7c78fd4b --- /dev/null +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -0,0 +1,367 @@ +// Autogenerated from Pigeon (v26.0.0), 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".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +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 BackgroundWorkerSettings { + BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds}); + + bool requiresCharging; + + int minimumDelaySeconds; + + List _toList() { + return [requiresCharging, minimumDelaySeconds]; + } + + Object encode() { + return _toList(); + } + + static BackgroundWorkerSettings decode(Object result) { + result as List; + return BackgroundWorkerSettings(requiresCharging: result[0]! as bool, minimumDelaySeconds: result[1]! as int); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! BackgroundWorkerSettings || 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 BackgroundWorkerSettings) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return BackgroundWorkerSettings.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class BackgroundWorkerFgHostApi { + /// Constructor for [BackgroundWorkerFgHostApi]. 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. + BackgroundWorkerFgHostApi({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 enable() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$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 configure(BackgroundWorkerSettings settings) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([settings]); + 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 disable() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$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; + } + } +} + +class BackgroundWorkerBgHostApi { + /// Constructor for [BackgroundWorkerBgHostApi]. 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. + BackgroundWorkerBgHostApi({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 onInitialized() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$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 showNotification(String title, String content) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, content]); + 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 close() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$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; + } + } +} + +abstract class BackgroundWorkerFlutterApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + Future onIosUpload(bool isRefresh, int? maxSeconds); + + Future onAndroidUpload(); + + Future cancel(); + + static void setUp( + BackgroundWorkerFlutterApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert( + message != null, + 'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null.', + ); + final List args = (message as List?)!; + final bool? arg_isRefresh = (args[0] as bool?); + assert( + arg_isRefresh != null, + 'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null, expected non-null bool.', + ); + final int? arg_maxSeconds = (args[1] as int?); + try { + await api.onIosUpload(arg_isRefresh!, arg_maxSeconds); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + await api.onAndroidUpload(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + await api.cancel(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + } +} diff --git a/mobile/lib/platform/background_worker_lock_api.g.dart b/mobile/lib/platform/background_worker_lock_api.g.dart new file mode 100644 index 0000000000..9f00017dc8 --- /dev/null +++ b/mobile/lib/platform/background_worker_lock_api.g.dart @@ -0,0 +1,97 @@ +// Autogenerated from Pigeon (v26.0.0), 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".', + ); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +class BackgroundWorkerLockApi { + /// Constructor for [BackgroundWorkerLockApi]. 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. + BackgroundWorkerLockApi({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 lock() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$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 unlock() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$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; + } + } +} diff --git a/mobile/lib/platform/connectivity_api.g.dart b/mobile/lib/platform/connectivity_api.g.dart new file mode 100644 index 0000000000..c348356f81 --- /dev/null +++ b/mobile/lib/platform/connectivity_api.g.dart @@ -0,0 +1,87 @@ +// Autogenerated from Pigeon (v26.0.0), 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".', + ); +} + +enum NetworkCapability { cellular, wifi, vpn, unmetered } + +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 NetworkCapability) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : NetworkCapability.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ConnectivityApi { + /// Constructor for [ConnectivityApi]. 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. + ConnectivityApi({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> getCapabilities() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$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(); + } + } +} diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index 6fc96f5046..01237f8c19 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -205,6 +205,45 @@ class SyncDelta { int get hashCode => Object.hashAll(_toList()); } +class HashResult { + HashResult({required this.assetId, this.error, this.hash}); + + String assetId; + + String? error; + + String? hash; + + List _toList() { + return [assetId, error, hash]; + } + + Object encode() { + return _toList(); + } + + static HashResult decode(Object result) { + result as List; + return HashResult(assetId: result[0]! as String, error: result[1] as String?, hash: result[2] as String?); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! HashResult || 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 @@ -221,6 +260,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is SyncDelta) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is HashResult) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -235,6 +277,8 @@ class _PigeonCodec extends StandardMessageCodec { return PlatformAlbum.decode(readValue(buffer)!); case 131: return SyncDelta.decode(readValue(buffer)!); + case 132: + return HashResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -468,15 +512,15 @@ class NativeSyncApi { } } - Future> hashPaths(List paths) async { + Future> hashAssets(List assetIds, {bool allowNetworkAccess = false}) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([paths]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds, allowNetworkAccess]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -492,7 +536,30 @@ class NativeSyncApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future cancelHashing() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$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; } } } diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index d3f0e3c1bc..491c38e7a8 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -2,6 +2,7 @@ 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/domain/models/asset/base_asset.model.dart'; @@ -135,7 +136,7 @@ class FeatInDevPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Features in Development'), centerTitle: true), + appBar: AppBar(title: Text('features_in_development'.tr()), centerTitle: true), body: Column( children: [ Flexible( diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 8ef3ef9757..5ec946858d 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; @RoutePage() class MainTimelinePage extends ConsumerWidget { @@ -12,22 +11,11 @@ class MainTimelinePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); - final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true)); - - return memoryLaneProvider.maybeWhen( - data: (memories) { - return memories.isEmpty || !memoriesEnabled - ? const Timeline() - : Timeline( - topSliverWidget: SliverToBoxAdapter( - key: Key('memory-lane-${memories.first.assets.first.id}'), - child: DriftMemoryLane(memories: memories), - ), - topSliverWidgetHeight: 200, - ); - }, - orElse: () => const Timeline(), + final hasMemories = ref.watch(driftMemoryFutureProvider.select((state) => state.value?.isNotEmpty ?? false)); + return Timeline( + topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()), + topSliverWidgetHeight: hasMemories ? 200 : 0, + showStorageIndicator: true, ); } } diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index b48d8fc307..4c18a09200 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -1,5 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -55,7 +56,7 @@ class LocalMediaSummaryPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Local Media Summary')), + appBar: AppBar(title: Text('local_media_summary'.tr())), body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); @@ -78,7 +79,7 @@ class LocalMediaSummaryPage extends StatelessWidget { const Divider(), Padding( padding: const EdgeInsets.only(left: 15), - child: Text("Album summary", style: ctx.textTheme.titleMedium), + child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium), ), ], ), @@ -135,7 +136,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Remote Media Summary')), + appBar: AppBar(title: Text('remote_media_summary'.tr())), body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); @@ -158,7 +159,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { const Divider(), Padding( padding: const EdgeInsets.only(left: 15), - child: Text("Album summary", style: ctx.textTheme.titleMedium), + child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium), ), ], ), diff --git a/mobile/lib/presentation/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart new file mode 100644 index 0000000000..e805458e76 --- /dev/null +++ b/mobile/lib/presentation/pages/download_info.page.dart @@ -0,0 +1,57 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/pages/common/download_panel.dart'; +import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; + +@RoutePage() +class DownloadInfoPage extends ConsumerWidget { + const DownloadInfoPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); + + onCancelDownload(String id) { + ref.watch(downloadStateProvider.notifier).cancelDownload(id); + } + + return Scaffold( + appBar: AppBar( + title: Text("download".t(context: context)), + actions: [], + ), + body: ListView.builder( + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: tasks.length, + itemBuilder: (context, index) { + final task = tasks[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: DownloadTaskTile( + progress: task.value.progress, + fileName: task.value.fileName, + status: task.value.status, + onCancelDownload: () => onCancelDownload(task.key), + ), + ); + }, + ), + persistentFooterButtons: [ + OutlinedButton( + onPressed: () { + tasks.map((e) => e.key).forEach(onCancelDownload); + }, + style: OutlinedButton.styleFrom(side: BorderSide(color: context.colorScheme.primary)), + child: Text( + 'clear_all'.t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index b8dcbd91f9..7f49a1ff79 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -208,7 +208,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { activityEnabled.value = value; await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value); }, - activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, + activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, dense: true, title: Text( "comments_and_likes", diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart new file mode 100644 index 0000000000..7a899f4e72 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -0,0 +1,352 @@ +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/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; + +@RoutePage() +class AssetTroubleshootPage extends ConsumerWidget { + final BaseAsset asset; + + const AssetTroubleshootPage({super.key, required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: Text('asset_troubleshoot'.tr())), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _AssetDetailsView(asset: asset), + ), + ), + ); + } +} + +class _AssetDetailsView extends ConsumerWidget { + final BaseAsset asset; + + const _AssetDetailsView({required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _AssetPropertiesSection(asset: asset), + const SizedBox(height: 16), + Text( + 'matching_assets'.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + if (asset.checksum != null) ...[ + _LocalAssetsSection(asset: asset), + const SizedBox(height: 16), + _RemoteAssetSection(asset: asset), + ] else ...[ + _PropertySectionCard( + title: 'Local Assets', + properties: [_PropertyItem(label: 'Status', value: 'no_checksum_local'.tr())], + ), + const SizedBox(height: 16), + _PropertySectionCard( + title: 'Remote Assets', + properties: [_PropertyItem(label: 'Status', value: 'no_checksum_remote'.tr())], + ), + ], + ], + ); + } +} + +class _AssetPropertiesSection extends ConsumerStatefulWidget { + final BaseAsset asset; + + const _AssetPropertiesSection({required this.asset}); + + @override + ConsumerState createState() => _AssetPropertiesSectionState(); +} + +class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection> { + List<_PropertyItem> properties = []; + + @override + void initState() { + super.initState(); + _buildAssetProperties(widget.asset).whenComplete(() { + if (mounted) { + setState(() {}); + } + }); + } + + @override + Widget build(BuildContext context) { + final title = _getAssetTypeTitle(widget.asset); + + return _PropertySectionCard(title: title, properties: properties); + } + + Future _buildAssetProperties(BaseAsset asset) async { + _addCommonProperties(); + + if (asset is LocalAsset) { + await _addLocalAssetProperties(asset); + } else if (asset is RemoteAsset) { + await _addRemoteAssetProperties(asset); + } + } + + void _addCommonProperties() { + final asset = widget.asset; + properties.addAll([ + _PropertyItem(label: 'Name', value: asset.name), + _PropertyItem(label: 'Checksum', value: asset.checksum), + _PropertyItem(label: 'Type', value: asset.type.toString()), + _PropertyItem(label: 'Created At', value: asset.createdAt.toString()), + _PropertyItem(label: 'Updated At', value: asset.updatedAt.toString()), + _PropertyItem(label: 'Width', value: asset.width?.toString()), + _PropertyItem(label: 'Height', value: asset.height?.toString()), + _PropertyItem( + label: 'Duration', + value: asset.durationInSeconds != null ? '${asset.durationInSeconds} seconds' : null, + ), + _PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()), + _PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId), + ]); + } + + Future _addLocalAssetProperties(LocalAsset asset) async { + properties.insertAll(0, [ + _PropertyItem(label: 'Local ID', value: asset.id), + _PropertyItem(label: 'Remote ID', value: asset.remoteId), + ]); + + properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString())); + final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id); + properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', '))); + } + + Future _addRemoteAssetProperties(RemoteAsset asset) async { + properties.insertAll(0, [ + _PropertyItem(label: 'Remote ID', value: asset.id), + _PropertyItem(label: 'Local ID', value: asset.localId), + _PropertyItem(label: 'Owner ID', value: asset.ownerId), + ]); + + final additionalProps = <_PropertyItem>[ + _PropertyItem(label: 'Thumb Hash', value: asset.thumbHash), + _PropertyItem(label: 'Visibility', value: asset.visibility.toString()), + _PropertyItem(label: 'Stack ID', value: asset.stackId), + ]; + + properties.insertAll(4, additionalProps); + + final exif = await ref.read(assetServiceProvider).getExif(asset); + if (exif != null) { + _addExifProperties(exif); + } else { + properties.add(const _PropertyItem(label: 'EXIF', value: null)); + } + } + + void _addExifProperties(ExifInfo exif) { + properties.addAll([ + _PropertyItem( + label: 'File Size', + value: exif.fileSize != null ? '${(exif.fileSize! / 1024 / 1024).toStringAsFixed(2)} MB' : null, + ), + _PropertyItem(label: 'Description', value: exif.description), + _PropertyItem(label: 'EXIF Width', value: exif.width?.toString()), + _PropertyItem(label: 'EXIF Height', value: exif.height?.toString()), + _PropertyItem(label: 'Date Taken', value: exif.dateTimeOriginal?.toString()), + _PropertyItem(label: 'Time Zone', value: exif.timeZone), + _PropertyItem(label: 'Camera Make', value: exif.make), + _PropertyItem(label: 'Camera Model', value: exif.model), + _PropertyItem(label: 'Lens', value: exif.lens), + _PropertyItem(label: 'F-Number', value: exif.f != null ? 'f/${exif.fNumber}' : null), + _PropertyItem(label: 'Focal Length', value: exif.mm != null ? '${exif.focalLength}mm' : null), + _PropertyItem(label: 'ISO', value: exif.iso?.toString()), + _PropertyItem(label: 'Exposure Time', value: exif.exposureTime.isNotEmpty ? exif.exposureTime : null), + _PropertyItem( + label: 'GPS Coordinates', + value: exif.hasCoordinates ? '${exif.latitude}, ${exif.longitude}' : null, + ), + _PropertyItem( + label: 'Location', + value: [exif.city, exif.state, exif.country].where((e) => e != null && e.isNotEmpty).join(', '), + ), + ]); + } + + String _getAssetTypeTitle(BaseAsset asset) { + if (asset is LocalAsset) return 'Local Asset'; + if (asset is RemoteAsset) return 'Remote Asset'; + return 'Base Asset'; + } +} + +class _LocalAssetsSection extends ConsumerWidget { + final BaseAsset asset; + + const _LocalAssetsSection({required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetService = ref.watch(assetServiceProvider); + + return FutureBuilder>( + future: assetService.getLocalAssetsByChecksum(asset.checksum!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _PropertySectionCard( + title: 'Local Assets', + properties: [_PropertyItem(label: 'Status', value: 'Loading...')], + ); + } + + if (snapshot.hasError) { + return _PropertySectionCard( + title: 'Local Assets', + properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())], + ); + } + + final localAssets = snapshot.data?.cast() ?? []; + if (asset is LocalAsset) { + localAssets.removeWhere((a) => a.id == (asset as LocalAsset).id); + + if (localAssets.isEmpty) { + return const SizedBox.shrink(); + } + } + + if (localAssets.isEmpty) { + return _PropertySectionCard( + title: 'Local Assets', + properties: [_PropertyItem(label: 'Status', value: 'no_local_assets_found'.tr())], + ); + } + + return Column( + children: [ + if (localAssets.length > 1) + _PropertySectionCard( + title: 'Local Assets Summary', + properties: [_PropertyItem(label: 'Total Count', value: localAssets.length.toString())], + ), + ...localAssets.map((localAsset) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: _AssetPropertiesSection(asset: localAsset), + ); + }), + ], + ); + }, + ); + } +} + +class _RemoteAssetSection extends ConsumerWidget { + final BaseAsset asset; + + const _RemoteAssetSection({required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetService = ref.watch(assetServiceProvider); + + if (asset is RemoteAsset) { + return const SizedBox.shrink(); + } + + return FutureBuilder( + future: assetService.getRemoteAssetByChecksum(asset.checksum!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _PropertySectionCard( + title: 'Remote Assets', + properties: [_PropertyItem(label: 'Status', value: 'Loading...')], + ); + } + + if (snapshot.hasError) { + return _PropertySectionCard( + title: 'Remote Assets', + properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())], + ); + } + + final remoteAsset = snapshot.data; + + if (remoteAsset == null) { + return _PropertySectionCard( + title: 'Remote Assets', + properties: [_PropertyItem(label: 'Status', value: 'no_remote_assets_found'.tr())], + ); + } + + return _AssetPropertiesSection(asset: remoteAsset); + }, + ); + } +} + +class _PropertySectionCard extends StatelessWidget { + final String title; + final List<_PropertyItem> properties; + + const _PropertySectionCard({required this.title, required this.properties}); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ...properties, + ], + ), + ), + ); + } +} + +class _PropertyItem extends StatelessWidget { + final String label; + final String? value; + + const _PropertyItem({required this.label, this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500)), + ), + Expanded( + child: Text( + value ?? 'not_available'.tr(), + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index af7014bbdc..d1d663e4f4 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -302,7 +303,9 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget { }).toList(); }, error: (error, _) { - return [Center(child: Text('Error: $error'))]; + return [ + Center(child: Text('error_saving_image'.tr(args: [error.toString()]))), + ]; }, loading: () { return [const Center(child: CircularProgressIndicator())]; diff --git a/mobile/lib/presentation/pages/drift_local_album.page.dart b/mobile/lib/presentation/pages/drift_local_album.page.dart index 536d9d82e2..6b133f7a6f 100644 --- a/mobile/lib/presentation/pages/drift_local_album.page.dart +++ b/mobile/lib/presentation/pages/drift_local_album.page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -46,9 +47,9 @@ class _AlbumList extends ConsumerWidget { ), data: (albums) { if (albums.isEmpty) { - return const SliverToBoxAdapter( + return SliverToBoxAdapter( child: Center( - child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text('no_albums_yet'.tr())), ), ); } diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index 30da6410b5..de8dde7714 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -25,9 +25,10 @@ class DriftMapPage extends StatelessWidget { onPressed: () => context.pop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), style: IconButton.styleFrom( - shape: const CircleBorder(side: BorderSide(width: 1, color: Colors.black26)), padding: const EdgeInsets.all(8), - backgroundColor: Colors.indigo.withValues(alpha: 0.7), + backgroundColor: Colors.indigo, + shadowColor: Colors.black26, + elevation: 4, ), ), ), diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index 95c5b008b3..f8a19b6b70 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; @RoutePage() class DriftPartnerDetailPage extends StatelessWidget { @@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { _inTimeline = !_inTimeline; }); } catch (error, stack) { - debugPrint("Failed to toggle in timeline: $error $stack"); + dPrint(() => "Failed to toggle in timeline: $error $stack"); ImmichToast.show( context: context, toastType: ToastType.error, diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart index c5c0b76988..d042f52673 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -1,5 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -38,14 +39,14 @@ class DriftPlacePage extends StatelessWidget { } } -class _PlaceSliverAppBar extends StatelessWidget { +class _PlaceSliverAppBar extends HookWidget { const _PlaceSliverAppBar({required this.search}); final ValueNotifier search; @override Widget build(BuildContext context) { - final searchFocusNode = FocusNode(); + final searchFocusNode = useFocusNode(); return SliverAppBar( floating: true, diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index 5bd32aaf81..b73913fd02 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -3,9 +3,8 @@ 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/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -26,11 +25,9 @@ final driftUsersProvider = FutureProvider.autoDispose>((ref) async id: entity.id, name: entity.name, email: entity.email, - isAdmin: entity.isAdmin, - updatedAt: entity.updatedAt, isPartnerSharedBy: false, isPartnerSharedWith: false, - avatarColor: AvatarColor.primary, + avatarColor: entity.avatarColor, memoryEnabled: true, inTimeline: true, profileChangedAt: entity.profileChangedAt, diff --git a/mobile/lib/presentation/pages/local_timeline.page.dart b/mobile/lib/presentation/pages/local_timeline.page.dart index c53b18d9e7..67bc17cb37 100644 --- a/mobile/lib/presentation/pages/local_timeline.page.dart +++ b/mobile/lib/presentation/pages/local_timeline.page.dart @@ -26,6 +26,7 @@ class LocalTimelinePage extends StatelessWidget { child: Timeline( appBar: MesmerizingSliverAppBar(title: album.name), bottomSheet: const LocalAlbumBottomSheet(), + showStorageIndicator: true, ), ); } diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index d44b215b03..7e70ebf8ff 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -440,7 +440,7 @@ class DriftSearchPage extends HookConsumerWidget { } }, icon: const Icon(Icons.more_vert_rounded), - tooltip: 'Show text search menu', + tooltip: 'show_text_search_menu'.tr(), ); }, menuChildren: [ @@ -633,6 +633,7 @@ class _SearchResultGrid extends ConsumerWidget { groupBy: GroupAssetsBy.none, appBar: null, bottomSheet: const GeneralBottomSheet(minChildSize: 0.20), + snapToMonth: false, ), ), ), diff --git a/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart new file mode 100644 index 0000000000..170f827fdb --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; + +class AdvancedInfoActionButton extends ConsumerWidget { + final ActionSource source; + + const AdvancedInfoActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + ref.read(actionProvider.notifier).troubleshoot(source, context); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + maxWidth: 115.0, + iconData: Icons.help_outline_rounded, + label: "troubleshoot".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart index 7c0db5ed9a..cb898f069a 100644 --- a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart @@ -1,54 +1,45 @@ -import 'package:fluttertoast/fluttertoast.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DownloadActionButton extends ConsumerWidget { final ActionSource source; + final bool menuItem; + const DownloadActionButton({super.key, required this.source, this.menuItem = false}); - const DownloadActionButton({super.key, required this.source}); - - void _onTap(BuildContext context, WidgetRef ref) async { + void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { if (!context.mounted) { return; } - final result = await ref.read(actionProvider.notifier).downloadAll(source); - ref.read(multiSelectProvider.notifier).reset(); + try { + await ref.read(actionProvider.notifier).downloadAll(source); - if (!context.mounted) { - return; - } - - if (!result.success) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } else if (result.count > 0) { - ImmichToast.show( - context: context, - msg: 'download_action_prompt'.t(context: context, args: {'count': result.count.toString()}), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.success, - ); + Future.delayed(const Duration(seconds: 1), () async { + await backgroundSyncManager.syncLocal(); + await backgroundSyncManager.hashAssets(); + }); + } finally { + ref.read(multiSelectProvider.notifier).reset(); } } @override Widget build(BuildContext context, WidgetRef ref) { + final backgroundManager = ref.watch(backgroundSyncProvider); + return BaseActionButton( iconData: Icons.download, maxWidth: 95, label: "download".t(context: context), - onPressed: () => _onTap(context, ref), + menuItem: menuItem, + onPressed: () => _onTap(context, ref, backgroundManager), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart new file mode 100644 index 0000000000..efa7f5c6d0 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart @@ -0,0 +1,64 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class DownloadStatusFloatingButton extends ConsumerWidget { + const DownloadStatusFloatingButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final shouldShow = ref.watch(downloadStateProvider.select((state) => state.showProgress)); + final itemCount = ref.watch(downloadStateProvider.select((state) => state.taskProgress.length)); + final isDownloading = ref + .watch(downloadStateProvider.select((state) => state.taskProgress)) + .values + .where((element) => element.progress != 1) + .isNotEmpty; + + return shouldShow + ? Badge.count( + count: itemCount, + textColor: context.colorScheme.onPrimary, + backgroundColor: context.colorScheme.primary, + child: FloatingActionButton( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), + side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), + ), + backgroundColor: context.isDarkTheme + ? context.colorScheme.surfaceContainer + : context.colorScheme.surfaceBright, + elevation: 2, + onPressed: () { + context.pushRoute(const DownloadInfoRoute()); + }, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + isDownloading + ? Icon(Icons.downloading_rounded, color: context.colorScheme.primary, size: 28) + : Icon( + Icons.download_done, + color: context.isDarkTheme ? Colors.green[200] : Colors.green[400], + size: 28, + ), + if (isDownloading) + const SizedBox( + height: 31, + width: 31, + child: CircularProgressIndicator( + strokeWidth: 2, + backgroundColor: Colors.transparent, + value: null, // Indeterminate progress + ), + ), + ], + ), + ), + ) + : const SizedBox.shrink(); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart index d143f600ce..33794eae11 100644 --- a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -58,7 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget { label: "like".t(context: context), menuItem: menuItem, ), - error: (error, stack) => Text("Error: $error"), + error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 546fbe408d..740ac528b0 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -1,15 +1,34 @@ import 'dart:io'; +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/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +class _SharePreparingDialog extends StatelessWidget { + const _SharePreparingDialog(); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), + ], + ), + ); + } +} + class ShareActionButton extends ConsumerWidget { final ActionSource source; @@ -20,28 +39,34 @@ class ShareActionButton extends ConsumerWidget { return; } - final result = await ref.read(actionProvider.notifier).shareAssets(source); - ref.read(multiSelectProvider.notifier).reset(); + showDialog( + context: context, + builder: (BuildContext buildContext) { + ref.read(actionProvider.notifier).shareAssets(source, context).then((ActionResult result) { + ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } + if (!context.mounted) { + return; + } - if (!result.success) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } else if (result.count > 0) { - ImmichToast.show( - context: context, - msg: 'share_action_prompt'.t(context: context, args: {'count': result.count.toString()}), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.success, - ); - } + if (!result.success) { + ImmichToast.show( + context: context, + msg: 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } + + buildContext.pop(); + }); + + // show a loading spinner with a "Preparing" message + return const _SharePreparingDialog(); + }, + barrierDismissible: false, + useRootNavigator: false, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart index ecc8a39c74..a07803ace5 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -36,7 +36,7 @@ class UnStackActionButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return BaseActionButton( - iconData: Icons.filter_none_rounded, + iconData: Icons.layers_clear_outlined, label: "unstack".t(context: context), onPressed: () => _onTap(context, ref), ); diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index e49f2b6804..f79b4bd7b1 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/album_filter.utils.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -39,8 +40,13 @@ class AlbumSelector extends ConsumerStatefulWidget { class _AlbumSelectorState extends ConsumerState { bool isGrid = false; final searchController = TextEditingController(); - QuickFilterMode filterMode = QuickFilterMode.all; + final menuController = MenuController(); final searchFocusNode = FocusNode(); + List sortedAlbums = []; + List shownAlbums = []; + + AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all); + AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true); @override void initState() { @@ -52,7 +58,7 @@ class _AlbumSelectorState extends ConsumerState { }); searchController.addListener(() { - onSearch(searchController.text, filterMode); + onSearch(searchController.text, filter.mode); }); searchFocusNode.addListener(() { @@ -62,9 +68,11 @@ class _AlbumSelectorState extends ConsumerState { }); } - void onSearch(String searchTerm, QuickFilterMode sortMode) { + void onSearch(String searchTerm, QuickFilterMode filterMode) { final userId = ref.watch(currentUserProvider)?.id; - ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode); + filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode); + + filterAlbums(); } Future onRefresh() async { @@ -77,17 +85,60 @@ class _AlbumSelectorState extends ConsumerState { }); } - void changeFilter(QuickFilterMode sortMode) { + void changeFilter(QuickFilterMode mode) { setState(() { - filterMode = sortMode; + filter = filter.copyWith(mode: mode); }); + + filterAlbums(); + } + + Future changeSort(AlbumSort sort) async { + setState(() { + this.sort = sort; + }); + + await sortAlbums(); } void clearSearch() { setState(() { - filterMode = QuickFilterMode.all; + filter = filter.copyWith(mode: QuickFilterMode.all, query: null); searchController.clear(); - ref.read(remoteAlbumProvider.notifier).clearSearch(); + }); + + filterAlbums(); + } + + Future sortAlbums() async { + final sorted = await ref + .read(remoteAlbumProvider.notifier) + .sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse); + + setState(() { + sortedAlbums = sorted; + }); + + // we need to re-filter the albums after sorting + // so shownAlbums gets updated + filterAlbums(); + } + + Future filterAlbums() async { + if (filter.query == null) { + setState(() { + shownAlbums = sortedAlbums; + }); + + return; + } + + final filteredAlbums = ref + .read(remoteAlbumProvider.notifier) + .searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode); + + setState(() { + shownAlbums = filteredAlbums; }); } @@ -100,36 +151,52 @@ class _AlbumSelectorState extends ConsumerState { @override Widget build(BuildContext context) { - final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums)); - final userId = ref.watch(currentUserProvider)?.id; - return MultiSliver( - children: [ - _SearchBar( - searchController: searchController, - searchFocusNode: searchFocusNode, - onSearch: onSearch, - filterMode: filterMode, - onClearSearch: clearSearch, - ), - _QuickFilterButtonRow( - filterMode: filterMode, - onChangeFilter: changeFilter, - onSearch: onSearch, - searchController: searchController, - ), - _QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode), - isGrid - ? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected) - : _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected), - ], + // refilter and sort when albums change + ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async { + await sortAlbums(); + }); + + return PopScope( + onPopInvokedWithResult: (didPop, _) { + menuController.close(); + }, + child: MultiSliver( + children: [ + _SearchBar( + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearch: onSearch, + filterMode: filter.mode, + onClearSearch: clearSearch, + ), + _QuickFilterButtonRow( + filterMode: filter.mode, + onChangeFilter: changeFilter, + onSearch: onSearch, + searchController: searchController, + ), + _QuickSortAndViewMode( + isGrid: isGrid, + onToggleViewMode: toggleViewMode, + onSortChanged: changeSort, + controller: menuController, + ), + isGrid + ? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected) + : _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected), + ], + ), ); } } class _SortButton extends ConsumerStatefulWidget { - const _SortButton(); + const _SortButton(this.onSortChanged, {this.controller}); + + final Future Function(AlbumSort) onSortChanged; + final MenuController? controller; @override ConsumerState<_SortButton> createState() => _SortButtonState(); @@ -148,15 +215,15 @@ class _SortButtonState extends ConsumerState<_SortButton> { albumSortIsReverse = !albumSortIsReverse; isSorting = true; }); - await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse); } else { setState(() { albumSortOption = sortMode; isSorting = true; }); - await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse); } + await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse)); + setState(() { isSorting = false; }); @@ -165,6 +232,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { @override Widget build(BuildContext context) { return MenuAnchor( + controller: widget.controller, style: MenuStyle( elevation: const WidgetStatePropertyAll(1), shape: WidgetStateProperty.all( @@ -394,10 +462,17 @@ class _QuickFilterButton extends StatelessWidget { } class _QuickSortAndViewMode extends StatelessWidget { - const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode}); + const _QuickSortAndViewMode({ + required this.isGrid, + required this.onToggleViewMode, + required this.onSortChanged, + this.controller, + }); final bool isGrid; final VoidCallback onToggleViewMode; + final MenuController? controller; + final Future Function(AlbumSort) onSortChanged; @override Widget build(BuildContext context) { @@ -407,7 +482,7 @@ class _QuickSortAndViewMode extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const _SortButton(), + _SortButton(onSortChanged, controller: controller), IconButton( icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), onPressed: onToggleViewMode, @@ -429,9 +504,9 @@ class _AlbumList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (albums.isEmpty) { - return const SliverToBoxAdapter( + return SliverToBoxAdapter( child: Center( - child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())), ), ); } @@ -524,9 +599,9 @@ class _AlbumGrid extends StatelessWidget { @override Widget build(BuildContext context) { if (albums.isEmpty) { - return const SliverToBoxAdapter( + return SliverToBoxAdapter( child: Center( - child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())), ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart index 1eb3366e30..dae6db568c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart @@ -2,11 +2,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier, BaseAsset?> { +class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier, BaseAsset> { @override - Future> build(BaseAsset? asset) async { - if (asset == null || asset is! RemoteAsset || asset.stackId == null) { - return const []; + Future> build(BaseAsset asset) { + if (asset is! RemoteAsset || asset.stackId == null) { + return Future.value(const []); } return ref.watch(assetServiceProvider).getStack(asset); @@ -14,4 +14,4 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier, BaseAsset?>(StackChildrenNotifier.new); + .family, BaseAsset>(StackChildrenNotifier.new); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart index e5d1487d53..0978b3c9af 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; -import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; class AssetStackRow extends ConsumerWidget { @@ -11,27 +11,25 @@ class AssetStackRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); - final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - - if (!showControls) { - opacity = 0; + final asset = ref.watch(assetViewerProvider.select((state) => state.currentAsset)); + if (asset == null) { + return const SizedBox.shrink(); } - final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); + final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull; + if (stackChildren == null || stackChildren.isEmpty) { + return const SizedBox.shrink(); + } + + final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + final opacity = showControls ? ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)) : 0; return IgnorePointer( ignoring: opacity < 255, child: AnimatedOpacity( opacity: opacity / 255, duration: Durations.short2, - child: ref - .watch(stackChildrenNotifier(asset)) - .when( - data: (state) => SizedBox.square(dimension: 80, child: _StackList(stack: state)), - error: (_, __) => const SizedBox.shrink(), - loading: () => const SizedBox.shrink(), - ), + child: _StackList(stack: stackChildren), ), ); } @@ -44,58 +42,77 @@ class _StackList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ListView.builder( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30), - itemCount: stack.length, - itemBuilder: (ctx, index) { - final asset = stack[index]; - return Padding( - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () { - ref.read(assetViewerProvider.notifier).setStackIndex(index); - ref.read(currentAssetNotifier.notifier).setAsset(asset); - }, - child: Container( - height: 60, - width: 60, - decoration: index == ref.watch(assetViewerProvider.select((s) => s.stackIndex)) - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), - ) - : const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Stack( - fit: StackFit.expand, - children: [ - Image( - fit: BoxFit.cover, - image: getThumbnailImageProvider(remoteId: asset.id, size: const Size.square(60)), - ), - if (asset.isVideo) - const Icon( - Icons.play_circle_outline_rounded, - color: Colors.white, - size: 16, - shadows: [ - Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0)), - ], - ), - ], - ), - ), - ), + return Center( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 5.0, + children: List.generate(stack.length, (i) { + final asset = stack[i]; + return _StackItem(key: ValueKey(asset.heroTag), asset: asset, index: i); + }), ), - ); - }, + ), + ), + ); + } +} + +class _StackItem extends ConsumerStatefulWidget { + final RemoteAsset asset; + final int index; + + const _StackItem({super.key, required this.asset, required this.index}); + + @override + ConsumerState<_StackItem> createState() => _StackItemState(); +} + +class _StackItemState extends ConsumerState<_StackItem> { + void _onTap() { + ref.read(currentAssetNotifier.notifier).setAsset(widget.asset); + ref.read(assetViewerProvider.notifier).setStackIndex(widget.index); + } + + @override + Widget build(BuildContext context) { + const playIcon = Center( + child: Icon( + Icons.play_circle_outline_rounded, + color: Colors.white, + size: 16, + shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], + ), + ); + const selectedDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), + borderRadius: BorderRadius.all(Radius.circular(10)), + ); + const unselectedDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide(color: Colors.grey, width: 0.5)), + borderRadius: BorderRadius.all(Radius.circular(10)), + ); + + Widget thumbnail = Thumbnail.fromAsset(asset: widget.asset, size: const Size(60, 40)); + if (widget.asset.isVideo) { + thumbnail = Stack(children: [thumbnail, playIcon]); + } + thumbnail = ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(10)), child: thumbnail); + final isSelected = ref.watch(assetViewerProvider.select((s) => s.stackIndex == widget.index)); + return SizedBox( + width: 60, + height: 40, + child: GestureDetector( + onTap: _onTap, + child: DecoratedBox( + decoration: isSelected ? selectedDecoration : unselectedDecoration, + position: DecorationPosition.foreground, + child: thumbnail, + ), + ), ); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 6c78cfac3e..7ac8cf34d5 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -3,13 +3,16 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; @@ -24,11 +27,11 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; -import 'package:platform/platform.dart'; @RoutePage() class AssetViewerPage extends StatelessWidget { @@ -51,13 +54,33 @@ class AssetViewerPage extends StatelessWidget { class AssetViewer extends ConsumerStatefulWidget { final int initialIndex; - final Platform? platform; final int? heroOffset; - const AssetViewer({super.key, required this.initialIndex, this.platform, this.heroOffset}); + const AssetViewer({super.key, required this.initialIndex, this.heroOffset}); @override ConsumerState createState() => _AssetViewerState(); + + static void setAsset(WidgetRef ref, BaseAsset asset) { + ref.read(assetViewerProvider.notifier).reset(); + _setAsset(ref, asset); + } + + void changeAsset(WidgetRef ref, BaseAsset asset) { + _setAsset(ref, asset); + } + + static void _setAsset(WidgetRef ref, BaseAsset asset) { + // Always holds the current asset from the timeline + ref.read(assetViewerProvider.notifier).setAsset(asset); + // The currentAssetNotifier actually holds the current asset that is displayed + // which could be stack children as well + ref.read(currentAssetNotifier.notifier).setAsset(asset); + if (asset.isVideo || asset.isMotionPhoto) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + } } const double _kBottomSheetMinimumExtent = 0.4; @@ -72,7 +95,6 @@ class _AssetViewerState extends ConsumerState { PhotoViewControllerBase? viewController; StreamSubscription? reloadSubscription; - late Platform platform; late final int heroOffset; late PhotoViewControllerValue initialPhotoViewState; bool? hasDraggedDown; @@ -95,18 +117,22 @@ class _AssetViewerState extends ConsumerState { ImageStream? _prevPreCacheStream; ImageStream? _nextPreCacheStream; + KeepAliveLink? _stackChildrenKeepAlive; + @override void initState() { super.initState(); + assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer"); pageController = PageController(initialPage: widget.initialIndex); - platform = widget.platform ?? const LocalPlatform(); totalAssets = ref.read(timelineServiceProvider).totalAssets; bottomSheetController = DraggableScrollableController(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _onAssetChanged(widget.initialIndex); - }); + WidgetsBinding.instance.addPostFrameCallback(_onAssetInit); reloadSubscription = EventStream.shared.listen(_onEvent); heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + final asset = ref.read(currentAssetNotifier); + if (asset != null) { + _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); + } } @override @@ -117,6 +143,8 @@ class _AssetViewerState extends ConsumerState { reloadSubscription?.cancel(); _prevPreCacheStream?.removeListener(_dummyListener); _nextPreCacheStream?.removeListener(_dummyListener); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + _stackChildrenKeepAlive?.close(); super.dispose(); } @@ -142,26 +170,9 @@ class _AssetViewerState extends ConsumerState { return provider.resolve(ImageConfiguration.empty)..addListener(_dummyListener); } - void _onAssetChanged(int index) async { - // Validate index bounds and try to get asset, loading buffer if needed + void _precacheAssets(int index) { final timelineService = ref.read(timelineServiceProvider); - final asset = await timelineService.getAssetAsync(index); - - if (asset == null) { - return; - } - - // Always holds the current asset from the timeline - ref.read(assetViewerProvider.notifier).setAsset(asset); - // The currentAssetNotifier actually holds the current asset that is displayed - // which could be stack children as well - ref.read(currentAssetNotifier.notifier).setAsset(asset); - if (asset.isVideo || asset.isMotionPhoto) { - ref.read(videoPlaybackValueProvider.notifier).reset(); - ref.read(videoPlayerControlsProvider.notifier).pause(); - } - - unawaited(ref.read(timelineServiceProvider).preCacheAssets(index)); + unawaited(timelineService.preCacheAssets(index)); _cancelTimers(); // This will trigger the pre-caching of adjacent assets ensuring // that they are ready when the user navigates to them. @@ -180,12 +191,31 @@ class _AssetViewerState extends ConsumerState { _nextPreCacheStream = nextAsset != null ? _precacheImage(nextAsset) : null; }); _delayedOperations.add(timer); - - _handleCasting(asset); } - void _handleCasting(BaseAsset asset) { + void _onAssetInit(Duration _) { + _precacheAssets(widget.initialIndex); + _handleCasting(); + } + + void _onAssetChanged(int index) async { + final timelineService = ref.read(timelineServiceProvider); + final asset = await timelineService.getAssetAsync(index); + if (asset == null) { + return; + } + + widget.changeAsset(ref, asset); + _precacheAssets(index); + _handleCasting(); + _stackChildrenKeepAlive?.close(); + _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); + } + + void _handleCasting() { if (!ref.read(castProvider).isCasting) return; + final asset = ref.read(currentAssetNotifier); + if (asset == null) return; // hide any casting snackbars if they exist context.scaffoldMessenger.hideCurrentSnackBar(); @@ -308,7 +338,7 @@ class _AssetViewerState extends ConsumerState { bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height); } - if (distanceToOrigin > openThreshold && !showingBottomSheet) { + if (distanceToOrigin > openThreshold && !showingBottomSheet && !ref.read(readonlyModeProvider)) { _openBottomSheet(ctx); } } @@ -507,7 +537,7 @@ class _AssetViewerState extends ConsumerState { BaseAsset displayAsset = asset; final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; if (stackChildren != null && stackChildren.isNotEmpty) { - displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex); } final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); @@ -584,6 +614,7 @@ class _AssetViewerState extends ConsumerState { // Rebuild the widget when the asset viewer state changes // Using multiple selectors to avoid unnecessary rebuilds for other state changes ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); + ref.watch(assetViewerProvider.select((s) => s.showingControls)); ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); ref.watch(assetViewerProvider.select((s) => s.stackIndex)); ref.watch(isPlayingMotionVideoProvider); @@ -596,10 +627,19 @@ class _AssetViewerState extends ConsumerState { if (asset == null) return; WidgetsBinding.instance.addPostFrameCallback((_) { - _handleCasting(asset); + _handleCasting(); }); }); + // Listen for control visibility changes and change system UI mode accordingly + ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async { + if (showingControls) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } + }); + // Currently it is not possible to scroll the asset when the bottom sheet is open all the way. // Issue: https://github.com/flutter/flutter/issues/109037 // TODO: Add a custom scrum builder once the fix lands on stable @@ -610,20 +650,25 @@ class _AssetViewerState extends ConsumerState { appBar: const ViewerTopAppBar(), extendBody: true, extendBodyBehindAppBar: true, - body: PhotoViewGallery.builder( - gaplessPlayback: true, - loadingBuilder: _placeholderBuilder, - pageController: pageController, - scrollPhysics: platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics(), // Use heavy physics for Android - itemCount: totalAssets, - onPageChanged: _onPageChanged, - onPageBuild: _onPageBuild, - scaleStateChangedCallback: _onScaleStateChanged, - builder: _assetBuilder, - backgroundDecoration: BoxDecoration(color: backgroundColor), - enablePanAlways: true, + floatingActionButton: const DownloadStatusFloatingButton(), + body: Stack( + children: [ + PhotoViewGallery.builder( + gaplessPlayback: true, + loadingBuilder: _placeholderBuilder, + pageController: pageController, + scrollPhysics: CurrentPlatform.isIOS + ? const FastScrollPhysics() // Use bouncing physics for iOS + : const FastClampingScrollPhysics(), // Use heavy physics for Android + itemCount: totalAssets, + onPageChanged: _onPageChanged, + onPageBuild: _onPageBuild, + scaleStateChangedCallback: _onScaleStateChanged, + builder: _assetBuilder, + backgroundDecoration: BoxDecoration(color: backgroundColor), + enablePanAlways: true, + ), + ], ), bottomNavigationBar: showingBottomSheet ? const SizedBox.shrink() diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart index 88513516eb..354902c9d7 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart @@ -68,21 +68,34 @@ class AssetViewerState { stackIndex.hashCode; } -class AssetViewerStateNotifier extends AutoDisposeNotifier { +class AssetViewerStateNotifier extends Notifier { @override AssetViewerState build() { return const AssetViewerState(); } + void reset() { + state = const AssetViewerState(); + } + void setAsset(BaseAsset? asset) { + if (asset == state.currentAsset) { + return; + } state = state.copyWith(currentAsset: asset, stackIndex: 0); } void setOpacity(int opacity) { + if (opacity == state.backgroundOpacity) { + return; + } state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity == 255 ? true : state.showingControls); } void setBottomSheet(bool showing) { + if (showing == state.showingBottomSheet) { + return; + } state = state.copyWith(showingBottomSheet: showing, showingControls: showing ? true : state.showingControls); if (showing) { ref.read(videoPlayerControlsProvider.notifier).pause(); @@ -90,6 +103,9 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier { } void setControls(bool isShowing) { + if (isShowing == state.showingControls) { + return; + } state = state.copyWith(showingControls: isShowing); } @@ -98,10 +114,11 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier { } void setStackIndex(int index) { + if (index == state.stackIndex) { + return; + } state = state.copyWith(stackIndex: index); } } -final assetViewerProvider = AutoDisposeNotifierProvider( - AssetViewerStateNotifier.new, -); +final assetViewerProvider = NotifierProvider(AssetViewerStateNotifier.new); diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 732afee7f9..3111512823 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_acti import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; @@ -26,6 +27,7 @@ class ViewerBottomBar extends ConsumerWidget { return const SizedBox.shrink(); } + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final user = ref.watch(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isSheetOpen = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); @@ -70,14 +72,14 @@ class ViewerBottomBar extends ConsumerWidget { ), ), child: Container( - height: context.padding.bottom + (asset.isVideo ? 160 : 90), color: Colors.black.withAlpha(125), - padding: EdgeInsets.only(bottom: context.padding.bottom), + padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ if (asset.isVideo) const VideoControls(), - if (!isInLockedView) Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + if (!isInLockedView && !isReadonlyModeEnabled) + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), ], ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index ccf6e0285e..2586789beb 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -1,17 +1,21 @@ 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/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.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'; @@ -39,6 +43,7 @@ class AssetDetailBottomSheet extends ConsumerWidget { final isInLockedView = ref.watch(inLockedViewProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider); final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; + final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); final buttonContext = ActionButtonContext( asset: asset, @@ -46,7 +51,9 @@ class AssetDetailBottomSheet extends ConsumerWidget { isArchived: isArchived, isTrashEnabled: isTrashEnable, isInLockedView: isInLockedView, + isStacked: asset.hasRemote && (asset as RemoteAsset).stackId != null, currentAlbum: currentAlbum, + advancedTroubleshooting: advancedTroubleshooting, source: ActionSource.viewer, ); @@ -120,6 +127,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget { return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); } + Future _editDateTime(BuildContext context, WidgetRef ref) async { + await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context); + } + @override Widget build(BuildContext context, WidgetRef ref) { final asset = ref.watch(currentAssetNotifier); @@ -130,10 +141,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget { final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final cameraTitle = _getCameraInfoTitle(exifInfo); - Future editDateTime() async { - await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context); - } - return SliverList.list( children: [ // Asset Date and Time @@ -141,7 +148,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { title: _getDateTime(context, asset), titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null, - onTap: asset.hasRemote ? () async => await editDateTime() : null, + onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null, ), if (exifInfo != null) _SheetAssetDescription(exif: exifInfo), const SheetPeopleDetails(), @@ -184,7 +191,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { } } -class _SheetTile extends StatelessWidget { +class _SheetTile extends ConsumerWidget { final String title; final Widget? leading; final Widget? trailing; @@ -203,8 +210,18 @@ class _SheetTile extends StatelessWidget { this.onTap, }); + void copyTitle(BuildContext context, WidgetRef ref) { + Clipboard.setData(ClipboardData(text: title)); + ImmichToast.show( + context: context, + msg: 'copied_to_clipboard'.t(context: context), + toastType: ToastType.info, + ); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + } + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final Widget titleWidget; if (leading == null) { titleWidget = LimitedBox( @@ -234,7 +251,7 @@ class _SheetTile extends StatelessWidget { return ListTile( dense: true, visualDensity: VisualDensity.compact, - title: titleWidget, + title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget), titleAlignment: ListTileTitleAlignment.center, leading: leading, trailing: trailing, diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart index fee34bca1b..64f22eca92 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart @@ -11,8 +11,8 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/people.utils.dart'; class SheetPeopleDetails extends ConsumerStatefulWidget { const SheetPeopleDetails({super.key}); @@ -158,11 +158,14 @@ class _PeopleAvatar extends StatelessWidget { maxLines: 1, ), if (person.birthDate != null) - Text( - formatAge(person.birthDate!, assetFileCreatedAt), - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium?.copyWith( - color: context.textTheme.bodyMedium?.color?.withAlpha(175), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + formatAge(person.birthDate!, assetFileCreatedAt), + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium?.copyWith( + color: context.textTheme.bodyMedium?.color?.withAlpha(175), + ), ), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index 411e279460..10c9821eb0 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; @@ -15,7 +16,9 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.sta import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -34,10 +37,12 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final user = ref.watch(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isInLockedView = ref.watch(inLockedViewProvider); + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final previousRouteName = ref.watch(previousRouteNameProvider); + final tabRoute = ref.watch(tabProvider); final showViewInTimelineButton = - previousRouteName != TabShellRoute.name && + (previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) && previousRouteName != AssetViewerRoute.name && previousRouteName != null; @@ -52,6 +57,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final actions = [ + if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true), if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true), if (album != null && album.isActivityEnabled && album.isShared) IconButton( @@ -94,7 +100,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { iconTheme: const IconThemeData(size: 22, color: Colors.white), actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), shape: const Border(), - actions: isShowingSheet + actions: isShowingSheet || isReadonlyModeEnabled ? null : isInLockedView ? lockedViewActions diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index a74c169224..8d374f74ff 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -65,7 +65,9 @@ class BackupToggleButtonState extends ConsumerState with Sin final uploadTasks = ref.watch(driftBackupProvider.select((state) => state.uploadItems)); - final isUploading = uploadTasks.isNotEmpty; + final isSyncing = ref.watch(driftBackupProvider.select((state) => state.isSyncing)); + + final isProcessing = uploadTasks.isNotEmpty || isSyncing; return AnimatedBuilder( animation: _animationController, @@ -129,7 +131,7 @@ class BackupToggleButtonState extends ConsumerState with Sin ], ), ), - child: isUploading + child: isProcessing ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) : Icon(Icons.cloud_upload_outlined, color: context.primaryColor, size: 24), ), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 45c602935d..f40e189e18 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; @@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -43,7 +44,8 @@ class ArchiveBottomSheet extends ConsumerWidget { const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - const StackActionButton(source: ActionSource.timeline), + if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), + if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ const DeleteLocalActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 3fb499f2a1..c7a0fbab40 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; @@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -43,7 +44,8 @@ class FavoriteBottomSheet extends ConsumerWidget { const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - const StackActionButton(source: ActionSource.timeline), + if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), + if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ const DeleteLocalActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index f1f092d2e2..9436707c84 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -4,6 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; @@ -17,10 +20,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -51,6 +56,7 @@ class _GeneralBottomSheetState extends ConsumerState { Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); + final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); Future addAssetsToAlbum(RemoteAlbum album) async { final selectedAssets = multiselect.selectedAssets; @@ -58,11 +64,19 @@ class _GeneralBottomSheetState extends ConsumerState { return; } + final remoteAssets = selectedAssets.whereType(); final addedCount = await ref .read(remoteAlbumProvider.notifier) - .addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList()); + .addAssets(album.id, remoteAssets.map((e) => e.id).toList()); - if (addedCount != selectedAssets.length) { + if (selectedAssets.length != remoteAssets.length) { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context), + ); + } + + if (addedCount != remoteAssets.length) { ImmichToast.show( context: context, msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), @@ -83,33 +97,39 @@ class _GeneralBottomSheetState extends ConsumerState { return BaseBottomSheet( controller: sheetController, - initialChildSize: 0.45, + initialChildSize: widget.minChildSize ?? 0.15, minChildSize: widget.minChildSize, maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ + if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[ + const AdvancedInfoActionButton(source: ActionSource.timeline), + ], const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), - const ArchiveActionButton(source: ActionSource.timeline), - const FavoriteActionButton(source: ActionSource.timeline), const DownloadActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), - const StackActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + const ArchiveActionButton(source: ActionSource.timeline), + const EditDateTimeActionButton(source: ActionSource.timeline), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton(source: ActionSource.timeline), + if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), + if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), const DeleteActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline), ], - slivers: [ - const AddToAlbumHeader(), - AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), - ], + slivers: multiselect.hasRemote + ? [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), + ] + : [], ); } } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart index 19cce3392f..ac3772a02b 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart @@ -1,22 +1,25 @@ 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/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; class MapBottomSheet extends StatelessWidget { const MapBottomSheet({super.key}); @override Widget build(BuildContext context) { - return const BaseBottomSheet( + return BaseBottomSheet( initialChildSize: 0.25, maxChildSize: 0.9, shouldCloseOnMinExtent: false, resizeOnScroll: false, actions: [], - slivers: [SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())], + backgroundColor: context.themeData.colorScheme.surface, + slivers: [const SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())], ); } } @@ -30,8 +33,13 @@ class _ScopedMapTimeline extends StatelessWidget { return ProviderScope( overrides: [ timelineServiceProvider.overrideWith((ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access archive'); + } + final bounds = ref.watch(mapStateProvider).bounds; - final timelineService = ref.watch(timelineFactoryProvider).map(bounds); + final timelineService = ref.watch(timelineFactoryProvider).map(user.id, bounds); ref.onDispose(timelineService.dispose); return timelineService; }), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 9f41a0c681..0ab419a56b 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -2,9 +2,11 @@ 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/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; @@ -15,23 +17,77 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class RemoteAlbumBottomSheet extends ConsumerWidget { +class RemoteAlbumBottomSheet extends ConsumerStatefulWidget { final RemoteAlbum album; const RemoteAlbumBottomSheet({super.key, required this.album}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _RemoteAlbumBottomSheetState(); +} + +class _RemoteAlbumBottomSheetState extends ConsumerState { + late DraggableScrollableController sheetController; + + @override + void initState() { + super.initState(); + sheetController = DraggableScrollableController(); + } + + @override + void dispose() { + sheetController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); + Future addAssetsToAlbum(RemoteAlbum album) async { + final selectedAssets = multiselect.selectedAssets; + if (selectedAssets.isEmpty) { + return; + } + + final addedCount = await ref + .read(remoteAlbumProvider.notifier) + .addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList()); + + if (addedCount != selectedAssets.length) { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}), + ); + } else { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + ); + } + + ref.read(multiSelectProvider.notifier).reset(); + } + + Future onKeyboardExpand() { + return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); + } + return BaseBottomSheet( - initialChildSize: 0.25, - maxChildSize: 0.4, + controller: sheetController, + initialChildSize: 0.22, + minChildSize: 0.22, + maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ const ShareActionButton(source: ActionSource.timeline), @@ -46,13 +102,18 @@ class RemoteAlbumBottomSheet extends ConsumerWidget { const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - const StackActionButton(source: ActionSource.timeline), + if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), + if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(source: ActionSource.timeline), ], - RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id), + RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), + ], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), ], ); } diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index d0428e5013..810340aeb8 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -50,12 +50,11 @@ mixin CancellableImageProviderMixin on CancellableImageProvide Stream loadRequest(ImageRequest request, ImageDecoderCallback decode) async* { if (isCancelled) { + this.request = null; evict(); return; } - this.request = request; - try { final image = await request.load(decode); if (image == null || isCancelled) { @@ -97,7 +96,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide final operation = cachedOperation; if (operation != null) { - this.cachedOperation = null; + cachedOperation = null; operation.cancel(); } } @@ -124,28 +123,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 return provider; } -ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = kThumbnailResolution}) { - assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided'); - - if (remoteId != null) { - return RemoteThumbProvider(assetId: remoteId); - } - - if (_shouldUseLocalAsset(asset!)) { +ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) { + if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type); } - final String assetId; - if (asset is LocalAsset && asset.hasRemote) { - assetId = asset.remoteId!; - } else if (asset is RemoteAsset) { - assetId = asset.id; - } else { - throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); - } - - return RemoteThumbProvider(assetId: assetId); + final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; + return assetId != null ? RemoteThumbProvider(assetId: assetId) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 8bdbe3c16a..f90961ea5a 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -4,6 +4,8 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; @@ -35,7 +37,8 @@ class LocalThumbProvider extends CancellableImageProvider } Stream _codec(LocalThumbProvider key, ImageDecoderCallback decode) { - return loadRequest(LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType), decode); + final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType); + return loadRequest(request, decode); } @override @@ -87,13 +90,26 @@ class LocalFullImageProvider extends CancellableImageProvider } Stream _codec(RemoteThumbProvider key, ImageDecoderCallback decode) { - final request = RemoteImageRequest( + final request = this.request = RemoteImageRequest( uri: getThumbnailUrlForRemoteId(key.assetId), headers: ApiService.getRequestHeaders(), cacheManager: cacheManager, @@ -92,16 +92,12 @@ class RemoteFullImageProvider extends CancellableImageProvider @override ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) { - return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode))..addOnLastListenerRemovedCallback(cancel); + return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode), onDispose: cancel); } Stream _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) { - return loadRequest(ThumbhashImageRequest(thumbhash: key.thumbHash), decode); + final request = this.request = ThumbhashImageRequest(thumbhash: key.thumbHash); + return loadRequest(request, decode); } @override diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 347d7efd3e..92b1bb2544 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; @@ -38,14 +38,7 @@ class Thumbnail extends StatefulWidget { ), _ => null, }, - imageProvider = switch (asset) { - RemoteAsset() => - asset.localId == null - ? RemoteThumbProvider(assetId: asset.id) - : LocalThumbProvider(id: asset.localId!, size: size, assetType: asset.type), - LocalAsset() => LocalThumbProvider(id: asset.id, size: size, assetType: asset.type), - _ => null, - }; + imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size); @override State createState() => _ThumbnailState(); @@ -94,7 +87,7 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix imageInfo.dispose(); return; } - + _fadeController.value = 1.0; setState(() { _providerImage = imageInfo.image; }); @@ -115,7 +108,7 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix final imageStream = _imageStream = imageProvider.resolve(ImageConfiguration.empty); final imageStreamListener = _imageStreamListener = ImageStreamListener( (ImageInfo imageInfo, bool synchronousCall) { - _stopListeningToStream(); + _stopListeningToThumbhashStream(); if (!mounted) { imageInfo.dispose(); return; @@ -125,7 +118,7 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix return; } - if (synchronousCall && _providerImage == null) { + if ((synchronousCall && _providerImage == null) || !_isVisible()) { _fadeController.value = 1.0; } else if (_fadeController.isAnimating) { _fadeController.forward(); @@ -201,6 +194,15 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix _loadFromThumbhashProvider(); } + bool _isVisible() { + final renderObject = context.findRenderObject() as RenderBox?; + if (renderObject == null || !renderObject.attached) return false; + + final topLeft = renderObject.localToGlobal(Offset.zero); + final bottomRight = renderObject.localToGlobal(Offset(renderObject.size.width, renderObject.size.height)); + return topLeft.dy < context.height && bottomRight.dy > 0; + } + @override Widget build(BuildContext context) { final colorScheme = context.colorScheme; @@ -226,6 +228,16 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix @override void dispose() { + final imageProvider = widget.imageProvider; + if (imageProvider is CancellableImageProvider) { + imageProvider.cancel(); + } + + final thumbhashProvider = widget.thumbhashProvider; + if (thumbhashProvider is CancellableImageProvider) { + thumbhashProvider.cancel(); + } + _fadeController.removeStatusListener(_onAnimationStatusChanged); _fadeController.dispose(); _stopListeningToStream(); @@ -306,7 +318,7 @@ class _ThumbnailRenderBox extends RenderBox { image: _previousImage!, fit: _fit, filterQuality: FilterQuality.low, - opacity: 1.0 - _fadeValue, + opacity: 1.0, ); } else if (_image == null || _fadeValue < 1.0) { final paint = Paint()..shader = _placeholderGradient.createShader(rect); diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index cfcb7a8985..a0163a7220 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -54,8 +54,6 @@ class ThumbnailTile extends ConsumerWidget { ) : const BoxDecoration(); - final hasStack = asset is RemoteAsset && asset.stackId != null; - final bool storageIndicator = showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))); @@ -77,21 +75,10 @@ class ThumbnailTile extends ConsumerWidget { child: Thumbnail.fromAsset(asset: asset, size: size), ), ), - if (hasStack) + if (asset != null) Align( alignment: Alignment.topRight, - child: Padding( - padding: EdgeInsets.only(right: 10.0, top: asset.isVideo ? 24.0 : 6.0), - child: const _TileOverlayIcon(Icons.burst_mode_rounded), - ), - ), - if (asset != null && asset.isVideo) - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(right: 10.0, top: 6.0), - child: _VideoIndicator(asset.duration), - ), + child: _AssetTypeIcons(asset: asset), ), if (storageIndicator && asset != null) switch (asset.storage) { @@ -214,3 +201,34 @@ class _TileOverlayIcon extends StatelessWidget { ); } } + +class _AssetTypeIcons extends StatelessWidget { + final BaseAsset asset; + + const _AssetTypeIcons({required this.asset}); + + @override + Widget build(BuildContext context) { + final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null; + final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (asset.isVideo) + Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)), + if (hasStack) + const Padding( + padding: EdgeInsets.only(right: 10.0, top: 6.0), + child: _TileOverlayIcon(Icons.burst_mode_rounded), + ), + if (isLivePhoto) + const Padding( + padding: EdgeInsets.only(right: 10.0, top: 6.0), + child: _TileOverlayIcon(Icons.motion_photos_on_rounded), + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 49af53f1eb..c1d5bf6464 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -9,8 +10,8 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; -import 'package:immich_mobile/presentation/widgets/map/map_utils.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; +import 'package:immich_mobile/presentation/widgets/map/map_utils.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -47,10 +48,12 @@ class _DriftMapState extends ConsumerState { MapLibreMapController? mapController; final _reloadMutex = AsyncMutex(); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); + final ValueNotifier bottomSheetOffset = ValueNotifier(0.25); @override void dispose() { _debouncer.dispose(); + bottomSheetOffset.dispose(); super.dispose(); } @@ -157,8 +160,8 @@ class _DriftMapState extends ConsumerState { return Stack( children: [ _Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady), - _MyLocationButton(onZoomToLocation: onZoomToLocation), - const MapBottomSheet(), + _DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset), + _DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset), ], ); } @@ -182,30 +185,66 @@ class _Map extends StatelessWidget { initialCameraPosition: initialLocation == null ? const CameraPosition(target: LatLng(0, 0), zoom: 0) : CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel), + compassEnabled: false, + rotateGesturesEnabled: false, styleString: style, onMapCreated: onMapCreated, onStyleLoadedCallback: onMapReady, + attributionButtonPosition: AttributionButtonPosition.topRight, + attributionButtonMargins: Platform.isIOS ? const Point(40, 12) : const Point(40, 72), ), ), ); } } -class _MyLocationButton extends StatelessWidget { - const _MyLocationButton({required this.onZoomToLocation}); +class _DynamicBottomSheet extends StatefulWidget { + final ValueNotifier bottomSheetOffset; - final VoidCallback onZoomToLocation; + const _DynamicBottomSheet({required this.bottomSheetOffset}); + @override + State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState(); +} + +class _DynamicBottomSheetState extends State<_DynamicBottomSheet> { @override Widget build(BuildContext context) { - return Positioned( - right: 0, - bottom: context.padding.bottom + 16, - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), + return NotificationListener( + onNotification: (notification) { + widget.bottomSheetOffset.value = notification.extent; + return true; + }, + child: const MapBottomSheet(), + ); + } +} + +class _DynamicMyLocationButton extends StatelessWidget { + const _DynamicMyLocationButton({required this.onZoomToLocation, required this.bottomSheetOffset}); + + final VoidCallback onZoomToLocation; + final ValueNotifier bottomSheetOffset; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: bottomSheetOffset, + builder: (context, offset, child) { + return Positioned( + right: 16, + bottom: context.height * (offset - 0.02) + context.padding.bottom, + child: AnimatedOpacity( + opacity: offset < 0.8 ? 1 : 0, + duration: const Duration(milliseconds: 150), + child: ElevatedButton( + onPressed: onZoomToLocation, + style: ElevatedButton.styleFrom(shape: const CircleBorder()), + child: const Icon(Icons.my_location), + ), + ), + ); + }, ); } } diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index ec49bbec96..b2c61c7488 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -7,15 +7,20 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart' import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class DriftMemoryLane extends ConsumerWidget { - final List memories; - - const DriftMemoryLane({super.key, required this.memories}); + const DriftMemoryLane({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); + final memories = memoryLaneProvider.value ?? const []; + if (memories.isEmpty) { + return const SizedBox.shrink(); + } + return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: CarouselView( @@ -38,7 +43,9 @@ class DriftMemoryLane extends ConsumerWidget { context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index)); }, - children: memories.map((memory) => DriftMemoryCard(memory: memory)).toList(), + children: memories + .map((memory) => DriftMemoryCard(key: Key(memory.id), memory: memory)) + .toList(growable: false), ), ); } diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index 8813224a5f..dd6390406b 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState(_selectedDate); } } catch (error) { - debugPrint('Error updating birthday: $error'); + dPrint(() => 'Error updating birthday: $error'); if (!context.mounted) { return; diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 46fd683b81..6de19000e0 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class DriftPersonNameEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState(newName); } } catch (error) { - debugPrint('Error updating name: $error'); + dPrint(() => 'Error updating name: $error'); if (!context.mounted) { return; diff --git a/mobile/lib/presentation/widgets/timeline/constants.dart b/mobile/lib/presentation/widgets/timeline/constants.dart index da78b5b02c..cfe96b1c81 100644 --- a/mobile/lib/presentation/widgets/timeline/constants.dart +++ b/mobile/lib/presentation/widgets/timeline/constants.dart @@ -2,7 +2,7 @@ import 'dart:ui'; const double kTimelineHeaderExtent = 80.0; const Size kTimelineFixedTileExtent = Size.square(256); -const Size kThumbnailResolution = kTimelineFixedTileExtent; // TODO: make the resolution vary based on actual tile size +const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size const double kTimelineSpacing = 2.0; const int kTimelineColumnCount = 3; diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index 05f96d49de..8121bb76d3 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart'; import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart'; @@ -15,6 +16,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -99,7 +101,6 @@ class _FixedSegmentRow extends ConsumerWidget { if (isScrubbing) { return _buildPlaceholder(context); } - if (timelineService.hasRange(assetIndex, assetCount)) { return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService); } @@ -154,6 +155,7 @@ class _AssetTileWidget extends ConsumerWidget { } else { await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1); ref.read(isPlayingMotionVideoProvider.notifier).playing = false; + AssetViewer.setAsset(ref, asset); ctx.pushRoute( AssetViewerRoute( initialIndex: assetIndex, @@ -190,11 +192,12 @@ class _AssetTileWidget extends ConsumerWidget { final lockSelection = _getLockSelectionStatus(ref); final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator)); + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); return RepaintBoundary( child: GestureDetector( onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset), - onLongPress: () => lockSelection ? null : _handleOnLongPress(ref, asset), + onLongPress: () => lockSelection || isReadonlyModeEnabled ? null : _handleOnLongPress(ref, asset), child: ThumbnailTile( asset, lockSelection: lockSelection, diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index b8c6668a38..3eff305251 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.dart @@ -7,9 +7,10 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -class TimelineHeader extends StatelessWidget { +class TimelineHeader extends HookConsumerWidget { final Bucket bucket; final HeaderType header; final double height; @@ -36,13 +37,12 @@ class TimelineHeader extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { if (bucket is! TimeBucket || header == HeaderType.none) { return const SizedBox.shrink(); } final date = (bucket as TimeBucket).date; - final isMonthHeader = header == HeaderType.month || header == HeaderType.monthAndDay; final isDayHeader = header == HeaderType.day || header == HeaderType.monthAndDay; @@ -57,7 +57,10 @@ class TimelineHeader extends StatelessWidget { if (isMonthHeader) Row( children: [ - Text(_formatMonth(context, date), style: context.textTheme.labelLarge?.copyWith(fontSize: 24)), + Text( + toBeginningOfSentenceCase(_formatMonth(context, date)), + style: context.textTheme.labelLarge?.copyWith(fontSize: 24), + ), const Spacer(), if (header != HeaderType.monthAndDay) _BulkSelectIconButton(bucket: bucket, assetOffset: assetOffset), ], @@ -65,7 +68,10 @@ class TimelineHeader extends StatelessWidget { if (isDayHeader) Row( children: [ - Text(_formatDay(context, date), style: context.textTheme.labelLarge?.copyWith(fontSize: 15)), + Text( + toBeginningOfSentenceCase(_formatDay(context, date)), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15), + ), const Spacer(), _BulkSelectIconButton(bucket: bucket, assetOffset: assetOffset), ], @@ -92,16 +98,19 @@ class _BulkSelectIconButton extends ConsumerWidget { bucketAssets = []; } + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets)); - return IconButton( - onPressed: () { - ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount); - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - }, - icon: isAllSelected - ? Icon(Icons.check_circle_rounded, size: 26, color: context.primaryColor) - : Icon(Icons.check_circle_outline_rounded, size: 26, color: context.colorScheme.onSurfaceSecondary), - ); + return isReadonlyModeEnabled + ? const SizedBox.shrink() + : IconButton( + onPressed: () { + ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount); + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + }, + icon: isAllSelected + ? Icon(Icons.check_circle_rounded, size: 26, color: context.primaryColor) + : Icon(Icons.check_circle_outline_rounded, size: 26, color: context.colorScheme.onSurfaceSecondary), + ); } } diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index be1d0f0873..58d7f933e9 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -3,12 +3,15 @@ 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/constants.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/utils/debounce.dart'; import 'package:intl/intl.dart' hide TextDirection; /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged @@ -28,6 +31,11 @@ class Scrubber extends ConsumerStatefulWidget { final double? monthSegmentSnappingOffset; + final bool snapToMonth; + + /// Whether an app bar is present, affects coordinate calculations + final bool hasAppBar; + Scrubber({ super.key, Key? scrollThumbKey, @@ -36,6 +44,8 @@ class Scrubber extends ConsumerStatefulWidget { this.topPadding = 0, this.bottomPadding = 0, this.monthSegmentSnappingOffset, + this.snapToMonth = true, + this.hasAppBar = true, required this.child, }) : assert(child.scrollDirection == Axis.vertical); @@ -44,7 +54,7 @@ class Scrubber extends ConsumerStatefulWidget { } List<_Segment> _buildSegments({required List layoutSegments, required double timelineHeight}) { - const double offsetThreshold = 20.0; + const double offsetThreshold = 40.0; final segments = <_Segment>[]; if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) { @@ -74,9 +84,13 @@ List<_Segment> _buildSegments({required List layoutSegments, required d } class ScrubberState extends ConsumerState with TickerProviderStateMixin { + String? _lastLabel; double _thumbTopOffset = 0.0; bool _isDragging = false; List<_Segment> _segments = []; + int _monthCount = 0; + DateTime? _currentScrubberDate; + Debouncer? _scrubberDebouncer; late AnimationController _thumbAnimationController; Timer? _fadeOutTimer; @@ -103,6 +117,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi _thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration); _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut); _labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration); + _monthCount = getMonthCount(); _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); } @@ -119,6 +134,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) { _segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight); + _monthCount = getMonthCount(); } } @@ -127,6 +143,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi _thumbAnimationController.dispose(); _labelAnimationController.dispose(); _fadeOutTimer?.cancel(); + _scrubberDebouncer?.dispose(); super.dispose(); } @@ -138,6 +155,10 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi }); } + int getMonthCount() { + return _segments.map((e) => "${e.date.month}_${e.date.year}").toSet().length; + } + bool _onScrollNotification(ScrollNotification notification) { if (_isDragging) { // If the user is dragging the thumb, we don't want to update the position @@ -166,12 +187,30 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi return false; } + void _onScrubberDateChanged(DateTime date) { + if (_currentScrubberDate != date) { + // Date changed, immediately set scrubbing to true + _currentScrubberDate = date; + ref.read(timelineStateProvider.notifier).setScrubbing(true); + + // Initialize debouncer if needed + _scrubberDebouncer ??= Debouncer(interval: const Duration(milliseconds: 50)); + + // Debounce setting scrubbing to false + _scrubberDebouncer!.run(() { + if (_currentScrubberDate == date) { + ref.read(timelineStateProvider.notifier).setScrubbing(false); + } + }); + } + } + void _onDragStart(DragStartDetails _) { - ref.read(timelineStateProvider.notifier).setScrubbing(true); setState(() { _isDragging = true; _labelAnimationController.forward(); _fadeOutTimer?.cancel(); + _lastLabel = null; }); } @@ -188,6 +227,25 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi final nearestMonthSegment = _findNearestMonthSegment(dragPosition); if (nearestMonthSegment != null) { + final label = nearestMonthSegment.scrollLabel; + if (_lastLabel != label) { + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + _lastLabel = label; + + // Notify timeline state of the new scrubber date position + if (_monthCount >= kMinMonthsToEnableScrubberSnap) { + _onScrubberDateChanged(nearestMonthSegment.date); + } + } + } + + if (_monthCount < kMinMonthsToEnableScrubberSnap || !widget.snapToMonth) { + // If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments + setState(() { + _thumbTopOffset = dragPosition; + _scrollController.jumpTo((dragPosition / _scrubberHeight) * _scrollController.position.maxScrollExtent); + }); + } else if (nearestMonthSegment != null) { _snapToSegment(nearestMonthSegment); } } @@ -208,14 +266,28 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi /// - If user drags to global Y position that's 100 pixels from the top /// - The relative position would be 100 - 50 = 50 (50 pixels into the scrubber area) double _calculateDragPosition(DragUpdateDetails details) { + if (widget.hasAppBar) { + final dragAreaTop = widget.topPadding; + final dragAreaBottom = widget.timelineHeight - widget.bottomPadding; + final dragAreaHeight = dragAreaBottom - dragAreaTop; + + final relativePosition = details.globalPosition.dy - dragAreaTop; + + // Make sure the position stays within the scrubber's bounds + return relativePosition.clamp(0.0, dragAreaHeight); + } + + // Get the local position relative to the gesture detector + final RenderBox? renderBox = context.findRenderObject() as RenderBox?; + if (renderBox != null) { + final localPosition = renderBox.globalToLocal(details.globalPosition); + return localPosition.dy.clamp(0.0, _scrubberHeight); + } + + // Fallback to current logic if render box is not available final dragAreaTop = widget.topPadding; - final dragAreaBottom = widget.timelineHeight - widget.bottomPadding; - final dragAreaHeight = dragAreaBottom - dragAreaTop; - final relativePosition = details.globalPosition.dy - dragAreaTop; - - // Make sure the position stays within the scrubber's bounds - return relativePosition.clamp(0.0, dragAreaHeight); + return relativePosition.clamp(0.0, _scrubberHeight); } /// Find the segment closest to the given position @@ -266,12 +338,18 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi } void _onDragEnd(DragEndDetails _) { - ref.read(timelineStateProvider.notifier).setScrubbing(false); _labelAnimationController.reverse(); setState(() { _isDragging = false; }); + ref.read(timelineStateProvider.notifier).setScrubbing(false); + + // Reset scrubber tracking when drag ends + _currentScrubberDate = null; + _scrubberDebouncer?.dispose(); + _scrubberDebouncer = null; + _resetThumbTimer(); } @@ -362,7 +440,7 @@ class _SegmentWidget extends StatelessWidget { Widget build(BuildContext context) { return IgnorePointer( child: Container( - margin: const EdgeInsets.only(right: 12.0), + margin: const EdgeInsets.only(right: 36.0), child: Material( color: context.colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(16.0)), diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index da1f7fcc9d..b3aec23f7f 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -72,8 +72,6 @@ class TimelineState { } class TimelineStateNotifier extends Notifier { - TimelineStateNotifier(); - void setScrubbing(bool isScrubbing) { state = state.copyWith(isScrubbing: isScrubbing); } diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index c859ae0e80..83e679b8c1 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -14,11 +14,13 @@ import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -31,27 +33,30 @@ class Timeline extends StatelessWidget { super.key, this.topSliverWidget, this.topSliverWidgetHeight, - this.showStorageIndicator, + this.showStorageIndicator = false, this.withStack = false, this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false), - this.bottomSheet = const GeneralBottomSheet(), + this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.18), this.groupBy, this.withScrubber = true, + this.snapToMonth = true, }); final Widget? topSliverWidget; final double? topSliverWidgetHeight; - final bool? showStorageIndicator; + final bool showStorageIndicator; final Widget? appBar; final Widget? bottomSheet; final bool withStack; final GroupAssetsBy? groupBy; final bool withScrubber; + final bool snapToMonth; @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, + floatingActionButton: const DownloadStatusFloatingButton(), body: LayoutBuilder( builder: (_, constraints) => ProviderScope( overrides: [ @@ -72,6 +77,7 @@ class Timeline extends StatelessWidget { appBar: appBar, bottomSheet: bottomSheet, withScrubber: withScrubber, + snapToMonth: snapToMonth, ), ), ), @@ -86,6 +92,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { this.appBar, this.bottomSheet, this.withScrubber = true, + this.snapToMonth = true, }); final Widget? topSliverWidget; @@ -93,13 +100,14 @@ class _SliverTimeline extends ConsumerStatefulWidget { final Widget? appBar; final Widget? bottomSheet; final bool withScrubber; + final bool snapToMonth; @override ConsumerState createState() => _SliverTimelineState(); } class _SliverTimelineState extends ConsumerState<_SliverTimeline> { - final _scrollController = ScrollController(); + late final ScrollController _scrollController; StreamSubscription? _eventSubscription; // Drag selection state @@ -111,10 +119,12 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { int _perRow = 4; double _scaleFactor = 3.0; double _baseScaleFactor = 3.0; + int? _scaleRestoreAssetIndex; @override void initState() { super.initState(); + _scrollController = ScrollController(onAttach: _restoreScalePosition); _eventSubscription = EventStream.shared.listen(_onEvent); final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow); @@ -128,7 +138,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { void _onEvent(Event event) { switch (event) { case ScrollToTopEvent(): - _scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); + ref.read(timelineStateProvider.notifier).setScrubbing(true); + _scrollController + .animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut) + .whenComplete(() => ref.read(timelineStateProvider.notifier).setScrubbing(false)); + case ScrollToDateEvent scrollToDateEvent: _scrollToDate(scrollToDateEvent.date); case TimelineReloadEvent(): @@ -142,6 +156,28 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); } + void _restoreScalePosition(_) { + if (_scaleRestoreAssetIndex == null) return; + + final asyncSegments = ref.read(timelineSegmentProvider); + asyncSegments.whenData((segments) { + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _scaleRestoreAssetIndex!); + if (targetSegment != null) { + final assetIndexInSegment = _scaleRestoreAssetIndex! - targetSegment.firstAssetIndex; + final newColumnCount = ref.read(timelineArgsProvider).columnCount; + final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); + final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; + final targetOffset = targetSegment.indexToLayoutOffset(targetRowIndex); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _scrollController.jumpTo(targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent)); + } + }); + } + }); + _scaleRestoreAssetIndex = null; + } + @override void dispose() { _scrollController.dispose(); @@ -176,11 +212,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { if (fallbackSegment != null) { // Scroll to the segment with a small offset to show the header final targetOffset = fallbackSegment.startOffset - 50; - _scrollController.animateTo( - targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); + ref.read(timelineStateProvider.notifier).setScrubbing(true); + _scrollController + .animateTo( + targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + .whenComplete(() => ref.read(timelineStateProvider.notifier).setScrubbing(false)); } }); } @@ -256,6 +295,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final maxHeight = ref.watch(timelineArgsProvider.select((args) => args.maxHeight)); final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable)); final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); return PopScope( canPop: !isMultiSelectEnabled, @@ -303,11 +343,13 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final Widget timeline; if (widget.withScrubber) { timeline = Scrubber( + snapToMonth: widget.snapToMonth, layoutSegments: segments, timelineHeight: maxHeight, topPadding: topPadding, bottomPadding: bottomPadding, monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight, + hasAppBar: widget.appBar != null, child: grid, ); } else { @@ -330,9 +372,28 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final newPerRow = 7 - newScaleFactor.toInt(); if (newPerRow != _perRow) { + final currentOffset = _scrollController.offset.clamp( + 0.0, + _scrollController.position.maxScrollExtent, + ); + final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; + int? targetAssetIndex; + if (segment != null) { + final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); + if (rowIndex > segment.firstIndex) { + final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); + final assetsPerRow = ref.read(timelineArgsProvider).columnCount; + final assetIndexInSegment = rowIndexInSegment * assetsPerRow; + targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; + } else { + targetAssetIndex = segment.firstAssetIndex; + } + } + setState(() { _scaleFactor = newScaleFactor; _perRow = newPerRow; + _scaleRestoreAssetIndex = targetAssetIndex; }); ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); @@ -342,9 +403,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { ), }, child: TimelineDragRegion( - onStart: _setDragStartIndex, + onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, onAssetEnter: _handleDragAssetEnter, - onEnd: _stopDrag, + onEnd: !isReadonlyModeEnabled ? _stopDrag : null, onScroll: _dragScroll, onScrollStart: () { // Minimize the bottom sheet when drag selection starts @@ -354,7 +415,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { children: [ timeline, if (!isSelectionMode && isMultiSelectEnabled) ...[ - const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()), + Positioned( + top: MediaQuery.paddingOf(context).top, + left: 25, + child: const SizedBox( + height: kToolbarHeight, + child: Center(child: _MultiSelectStatusButton()), + ), + ), if (widget.bottomSheet != null) widget.bottomSheet!, ], ], diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 0696a8d7f1..29de09fd33 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; @@ -15,11 +15,11 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; @@ -33,6 +33,12 @@ class AppLifeCycleNotifier extends StateNotifier { final Ref _ref; bool _wasPaused = false; + // Add operation coordination + Completer? _resumeOperation; + Completer? _pauseOperation; + + final _log = Logger("AppLifeCycleNotifier"); + AppLifeCycleNotifier(this._ref) : super(AppLifeCycleEnum.active); AppLifeCycleEnum getAppState() { @@ -42,6 +48,32 @@ class AppLifeCycleNotifier extends StateNotifier { void handleAppResume() async { state = AppLifeCycleEnum.resumed; + // Prevent overlapping resume operations + if (_resumeOperation != null && !_resumeOperation!.isCompleted) { + await _resumeOperation!.future; + return; + } + + // Cancel any ongoing pause operation + if (_pauseOperation != null && !_pauseOperation!.isCompleted) { + _pauseOperation!.complete(); + } + + _resumeOperation = Completer(); + + try { + await _performResume(); + } catch (e, stackTrace) { + _log.severe("Error during app resume", e, stackTrace); + } finally { + if (!_resumeOperation!.isCompleted) { + _resumeOperation!.complete(); + } + _resumeOperation = null; + } + } + + Future _performResume() async { // no need to resume because app was never really paused if (!_wasPaused) return; _wasPaused = false; @@ -52,9 +84,7 @@ class AppLifeCycleNotifier extends StateNotifier { if (isAuthenticated) { // switch endpoint if needed final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); - if (kDebugMode) { - debugPrint("Using server URL: $endpoint"); - } + _log.info("Using server URL: $endpoint"); if (!Store.isBetaTimelineEnabled) { final permission = _ref.watch(galleryPermissionNotifier); @@ -80,40 +110,10 @@ class AppLifeCycleNotifier extends StateNotifier { break; } } else { - _ref.read(backupProvider.notifier).cancelBackup(); - - final backgroundManager = _ref.read(backgroundSyncProvider); - // Ensure proper cleanup before starting new background tasks - try { - await Future.wait([ - Future(() async { - await backgroundManager.syncLocal(); - Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal"); - // Check if app is still active before hashing - if ([AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state)) { - await backgroundManager.hashAssets(); - } - }), - backgroundManager.syncRemote(), - ]).then((_) async { - final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); - - if (isEnableBackup) { - final currentUser = _ref.read(currentUserProvider); - if (currentUser == null) { - return; - } - - await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); - } - }); - } catch (e, stackTrace) { - Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace); - } + _ref.read(websocketProvider.notifier).connect(); + await _handleBetaTimelineResume(); } - _ref.read(websocketProvider.notifier).connect(); - await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); @@ -125,15 +125,112 @@ class AppLifeCycleNotifier extends StateNotifier { } } + Future _safeRun(Future action, String debugName) async { + if (!_shouldContinueOperation()) { + return; + } + + try { + await action; + } catch (e, stackTrace) { + _log.warning("Error during $debugName operation", e, stackTrace); + } + } + + Future _handleBetaTimelineResume() async { + _ref.read(backupProvider.notifier).cancelBackup(); + unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock()); + + // Give isolates time to complete any ongoing database transactions + await Future.delayed(const Duration(milliseconds: 500)); + + final backgroundManager = _ref.read(backgroundSyncProvider); + final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); + + try { + bool syncSuccess = false; + await Future.wait([ + _safeRun(backgroundManager.syncLocal(), "syncLocal"), + _safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"), + ]); + if (syncSuccess) { + await Future.wait([ + _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { + _resumeBackup(); + }), + _resumeBackup(), + ]); + } else { + _ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed); + await _safeRun(backgroundManager.hashAssets(), "hashAssets"); + } + + if (isAlbumLinkedSyncEnable) { + await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum"); + } + } catch (e, stackTrace) { + _log.severe("Error during background sync", e, stackTrace); + } + } + + Future _resumeBackup() async { + final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + + if (isEnableBackup) { + final currentUser = Store.tryGet(StoreKey.currentUser); + if (currentUser != null) { + await _safeRun( + _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id), + "handleBackupResume", + ); + } + } + } + + // Helper method to check if operations should continue + bool _shouldContinueOperation() { + return [AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state) && + (_resumeOperation?.isCompleted == false || _resumeOperation == null); + } + void handleAppInactivity() { state = AppLifeCycleEnum.inactive; // do not stop/clean up anything on inactivity: issued on every orientation change } - void handleAppPause() { + Future handleAppPause() async { state = AppLifeCycleEnum.paused; _wasPaused = true; + // Prevent overlapping pause operations + if (_pauseOperation != null && !_pauseOperation!.isCompleted) { + await _pauseOperation!.future; + return; + } + + // Cancel any ongoing resume operation + if (_resumeOperation != null && !_resumeOperation!.isCompleted) { + _resumeOperation!.complete(); + } + + _pauseOperation = Completer(); + + try { + if (Store.isBetaTimelineEnabled) { + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); + } + await _performPause(); + } catch (e, stackTrace) { + _log.severe("Error during app pause", e, stackTrace); + } finally { + if (!_pauseOperation!.isCompleted) { + _pauseOperation!.complete(); + } + _pauseOperation = null; + } + } + + Future _performPause() async { if (_ref.read(authProvider).isAuthenticated) { if (!Store.isBetaTimelineEnabled) { // Do not cancel backup if manual upload is in progress @@ -147,20 +244,20 @@ class AppLifeCycleNotifier extends StateNotifier { try { LogService.I.flush(); - } catch (e) { - // Ignore flush errors during pause - } + } catch (_) {} } Future handleAppDetached() async { state = AppLifeCycleEnum.detached; + if (Store.isBetaTimelineEnabled) { + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); + } + // Flush logs before closing database try { LogService.I.flush(); - } catch (e) { - // Ignore flush errors during shutdown - } + } catch (_) {} // Close Isar database safely try { @@ -168,9 +265,7 @@ class AppLifeCycleNotifier extends StateNotifier { if (isar != null && isar.isOpen) { await isar.close(); } - } catch (e) { - // Ignore close errors during shutdown - } + } catch (_) {} if (Store.isBetaTimelineEnabled) { return; @@ -179,9 +274,7 @@ class AppLifeCycleNotifier extends StateNotifier { // no guarantee this is called at all try { _ref.read(manualUploadProvider.notifier).cancelBackup(); - } catch (e) { - // Ignore errors during shutdown - } + } catch (_) {} } void handleAppHidden() { diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index 9c8b28e6cf..75635950ff 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -1,4 +1,3 @@ -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'; @@ -13,6 +12,7 @@ import 'package:immich_mobile/services/etag.service.dart'; import 'package:immich_mobile/services/exif.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final assetProvider = StateNotifierProvider((ref) { return AssetNotifier( @@ -68,7 +68,7 @@ class AssetNotifier extends StateNotifier { } final bool newRemote = await _assetService.refreshRemoteAssets(); final bool newLocal = await _albumService.refreshDeviceAlbums(); - debugPrint("changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); + dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); if (newRemote) { _ref.invalidate(memoryFutureProvider); } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 6c39eb0dec..9a15598998 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -1,4 +1,3 @@ -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'; @@ -18,6 +17,7 @@ import 'package:immich_mobile/services/widget.service.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final authProvider = StateNotifierProvider((ref) { return AuthNotifier( @@ -150,10 +150,7 @@ class AuthNotifier extends StateNotifier { _log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace); } catch (error, stackTrace) { _log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace); - - if (kDebugMode) { - debugPrint("Error getting user information from the server [CATCH ALL] $error $stackTrace"); - } + dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace"); } // If the user is null, the login was not successful diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 76cb383465..03666466ff 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -2,8 +2,6 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -33,6 +31,7 @@ import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final backupProvider = StateNotifierProvider((ref) { return BackupNotifier( @@ -286,7 +285,7 @@ class BackupNotifier extends StateNotifier { state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); + dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); } /// @@ -428,7 +427,7 @@ class BackupNotifier extends StateNotifier { /// Invoke backup process Future startBackupProcess() async { - debugPrint("Start backup process"); + dPrint(() => "Start backup process"); assert(state.backupProgress == BackUpProgressEnum.idle); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index 418410de0c..f52fc654f2 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -1,18 +1,18 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:async'; -import 'dart:convert'; import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; class EnqueueStatus { @@ -36,6 +36,7 @@ class DriftUploadStatus { final int fileSize; final String networkSpeedAsString; final bool? isFailed; + final String? error; const DriftUploadStatus({ required this.taskId, @@ -44,6 +45,7 @@ class DriftUploadStatus { required this.fileSize, required this.networkSpeedAsString, this.isFailed, + this.error, }); DriftUploadStatus copyWith({ @@ -53,6 +55,7 @@ class DriftUploadStatus { int? fileSize, String? networkSpeedAsString, bool? isFailed, + String? error, }) { return DriftUploadStatus( taskId: taskId ?? this.taskId, @@ -61,12 +64,13 @@ class DriftUploadStatus { fileSize: fileSize ?? this.fileSize, networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString, isFailed: isFailed ?? this.isFailed, + error: error ?? this.error, ); } @override String toString() { - return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString, isFailed: $isFailed)'; + return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString, isFailed: $isFailed, error: $error)'; } @override @@ -78,7 +82,8 @@ class DriftUploadStatus { other.progress == progress && other.fileSize == fileSize && other.networkSpeedAsString == networkSpeedAsString && - other.isFailed == isFailed; + other.isFailed == isFailed && + other.error == error; } @override @@ -88,46 +93,25 @@ class DriftUploadStatus { progress.hashCode ^ fileSize.hashCode ^ networkSpeedAsString.hashCode ^ - isFailed.hashCode; + isFailed.hashCode ^ + error.hashCode; } - - Map toMap() { - return { - 'taskId': taskId, - 'filename': filename, - 'progress': progress, - 'fileSize': fileSize, - 'networkSpeedAsString': networkSpeedAsString, - 'isFailed': isFailed, - }; - } - - factory DriftUploadStatus.fromMap(Map map) { - return DriftUploadStatus( - taskId: map['taskId'] as String, - filename: map['filename'] as String, - progress: map['progress'] as double, - fileSize: map['fileSize'] as int, - networkSpeedAsString: map['networkSpeedAsString'] as String, - isFailed: map['isFailed'] != null ? map['isFailed'] as bool : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory DriftUploadStatus.fromJson(String source) => - DriftUploadStatus.fromMap(json.decode(source) as Map); } +enum BackupError { none, syncFailed } + class DriftBackupState { final int totalCount; final int backupCount; final int remainderCount; + final int processingCount; final int enqueueCount; final int enqueueTotalCount; + final bool isSyncing; final bool isCanceling; + final BackupError error; final Map uploadItems; @@ -135,35 +119,44 @@ class DriftBackupState { required this.totalCount, required this.backupCount, required this.remainderCount, + required this.processingCount, required this.enqueueCount, required this.enqueueTotalCount, required this.isCanceling, + required this.isSyncing, required this.uploadItems, + this.error = BackupError.none, }); DriftBackupState copyWith({ int? totalCount, int? backupCount, int? remainderCount, + int? processingCount, int? enqueueCount, int? enqueueTotalCount, bool? isCanceling, + bool? isSyncing, Map? uploadItems, + BackupError? error, }) { return DriftBackupState( totalCount: totalCount ?? this.totalCount, backupCount: backupCount ?? this.backupCount, remainderCount: remainderCount ?? this.remainderCount, + processingCount: processingCount ?? this.processingCount, enqueueCount: enqueueCount ?? this.enqueueCount, enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount, isCanceling: isCanceling ?? this.isCanceling, + isSyncing: isSyncing ?? this.isSyncing, uploadItems: uploadItems ?? this.uploadItems, + error: error ?? this.error, ); } @override String toString() { - return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)'; + return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, isSyncing: $isSyncing, uploadItems: $uploadItems, error: $error)'; } @override @@ -174,10 +167,13 @@ class DriftBackupState { return other.totalCount == totalCount && other.backupCount == backupCount && other.remainderCount == remainderCount && + other.processingCount == processingCount && other.enqueueCount == enqueueCount && other.enqueueTotalCount == enqueueTotalCount && other.isCanceling == isCanceling && - mapEquals(other.uploadItems, uploadItems); + other.isSyncing == isSyncing && + mapEquals(other.uploadItems, uploadItems) && + other.error == error; } @override @@ -185,10 +181,13 @@ class DriftBackupState { return totalCount.hashCode ^ backupCount.hashCode ^ remainderCount.hashCode ^ + processingCount.hashCode ^ enqueueCount.hashCode ^ enqueueTotalCount.hashCode ^ isCanceling.hashCode ^ - uploadItems.hashCode; + isSyncing.hashCode ^ + uploadItems.hashCode ^ + error.hashCode; } } @@ -203,10 +202,13 @@ class DriftBackupNotifier extends StateNotifier { totalCount: 0, backupCount: 0, remainderCount: 0, + processingCount: 0, enqueueCount: 0, enqueueTotalCount: 0, isCanceling: false, + isSyncing: false, uploadItems: {}, + error: BackupError.none, ), ) { { @@ -235,7 +237,9 @@ class DriftBackupNotifier extends StateNotifier { switch (update.status) { case TaskStatus.complete: if (update.task.group == kBackupGroup) { - state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1); + if (update.responseStatusCode == 201) { + state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1); + } } // Remove the completed task from the upload items @@ -257,7 +261,24 @@ class DriftBackupNotifier extends StateNotifier { return; } - state = state.copyWith(uploadItems: {...state.uploadItems, taskId: currentItem.copyWith(isFailed: true)}); + String? error; + final exception = update.exception; + if (exception != null && exception is TaskHttpException) { + final message = tryJsonDecode(exception.description)?['message'] as String?; + if (message != null) { + final responseCode = exception.httpResponseCode; + error = "${exception.exceptionType}, response code $responseCode: $message"; + } + } + error ??= update.exception?.toString(); + + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + taskId: currentItem.copyWith(isFailed: true, error: error), + }, + ); + _logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}"); break; case TaskStatus.canceled: @@ -311,16 +332,26 @@ class DriftBackupNotifier extends StateNotifier { } Future getBackupStatus(String userId) async { - final [totalCount, backupCount, remainderCount] = await Future.wait([ - _uploadService.getBackupTotalCount(), - _uploadService.getBackupFinishedCount(userId), - _uploadService.getBackupRemainderCount(userId), - ]); + final counts = await _uploadService.getBackupCounts(userId); - state = state.copyWith(totalCount: totalCount, backupCount: backupCount, remainderCount: remainderCount); + state = state.copyWith( + totalCount: counts.total, + backupCount: counts.total - counts.remainder, + remainderCount: counts.remainder, + processingCount: counts.processing, + ); + } + + void updateError(BackupError error) async { + state = state.copyWith(error: error); + } + + void updateSyncing(bool isSyncing) async { + state = state.copyWith(isSyncing: isSyncing); } Future startBackup(String userId) { + state = state.copyWith(error: BackupError.none); return _uploadService.startBackup(userId, _updateEnqueueCount); } @@ -329,16 +360,16 @@ class DriftBackupNotifier extends StateNotifier { } Future cancel() async { - debugPrint("Canceling backup tasks..."); - state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true); + dPrint(() => "Canceling backup tasks..."); + state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none); final activeTaskCount = await _uploadService.cancelBackup(); if (activeTaskCount > 0) { - debugPrint("$activeTaskCount tasks left, continuing to cancel..."); + dPrint(() => "$activeTaskCount tasks left, continuing to cancel..."); await cancel(); } else { - debugPrint("All tasks canceled successfully."); + dPrint(() => "All tasks canceled successfully."); // Clear all upload items when cancellation is complete state = state.copyWith(isCanceling: false, uploadItems: {}); } @@ -346,6 +377,7 @@ class DriftBackupNotifier extends StateNotifier { Future handleBackupResume(String userId) async { _logger.info("Resuming backup tasks..."); + state = state.copyWith(error: BackupError.none); final tasks = await _uploadService.getActiveTasks(kBackupGroup); _logger.info("Found ${tasks.length} tasks"); @@ -373,12 +405,12 @@ final driftBackupCandidateProvider = FutureProvider.autoDispose return []; } - return ref.read(backupRepositoryProvider).getCandidates(user.id); + return ref.read(backupRepositoryProvider).getCandidates(user.id, onlyHashed: false); }); final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family, String>(( ref, assetId, ) { - return ref.read(backupRepositoryProvider).getSourceAlbums(assetId); + return ref.read(localAssetRepository).getSourceAlbums(assetId, backupSelection: BackupSelection.selected); }); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index 1aea7f64cb..bfc079bfa3 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -30,6 +30,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final manualUploadProvider = StateNotifierProvider((ref) { return ManualUploadNotifier( @@ -216,7 +217,7 @@ class ManualUploadNotifier extends StateNotifier { ); if (uploadAssets.isEmpty) { - debugPrint("[_startUpload] No Assets to upload - Abort Process"); + dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); return false; } @@ -294,10 +295,10 @@ class ManualUploadNotifier extends StateNotifier { } } else { openAppSettings(); - debugPrint("[_startUpload] Do not have permission to the gallery"); + dPrint(() => "[_startUpload] Do not have permission to the gallery"); } } catch (e) { - debugPrint("ERROR _startUpload: ${e.toString()}"); + dPrint(() => "ERROR _startUpload: ${e.toString()}"); hasErrors = true; } finally { _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); @@ -340,7 +341,7 @@ class ManualUploadNotifier extends StateNotifier { // waits until it has stopped to start the backup. final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); if (!hasLock) { - debugPrint("[uploadAssets] could not acquire lock, exiting"); + dPrint(() => "[uploadAssets] could not acquire lock, exiting"); ImmichToast.show( context: context, msg: "failed".tr(), @@ -355,18 +356,18 @@ class ManualUploadNotifier extends StateNotifier { // check if backup is already in process - then return if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - debugPrint("[uploadAssets] Manual upload is already running - abort"); + dPrint(() => "[uploadAssets] Manual upload is already running - abort"); showInProgress = true; } if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - debugPrint("[uploadAssets] Auto Backup is already in progress - abort"); + dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); showInProgress = true; return false; } if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - debugPrint("[uploadAssets] Background backup is running - abort"); + dPrint(() => "[uploadAssets] Background backup is running - abort"); showInProgress = true; } diff --git a/mobile/lib/providers/image/cache/remote_image_cache_manager.dart b/mobile/lib/providers/image/cache/remote_image_cache_manager.dart index df5f4566c0..41c541ccdb 100644 --- a/mobile/lib/providers/image/cache/remote_image_cache_manager.dart +++ b/mobile/lib/providers/image/cache/remote_image_cache_manager.dart @@ -38,9 +38,21 @@ abstract class RemoteCacheManager extends CacheManager { final file = await store.fileSystem.createFile(path); final sink = file.openWrite(); try { - await source.pipe(sink); + await source.listen(sink.add, cancelOnError: true).asFuture(); } catch (e) { + try { + await sink.close(); + await file.delete(); + } catch (e) { + _log.severe('Failed to delete incomplete cache file: $e'); + } + return; + } + + try { + await sink.flush(); await sink.close(); + } catch (e) { try { await file.delete(); } catch (e) { diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 21a22e7e5f..77ac6595a7 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -1,11 +1,16 @@ +import 'package:auto_route/auto_route.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/services/timeline.service.dart'; @@ -34,6 +39,7 @@ class ActionNotifier extends Notifier { late ActionService _service; late UploadService _uploadService; late DownloadService _downloadService; + late AssetService _assetService; ActionNotifier() : super(); @@ -41,6 +47,7 @@ class ActionNotifier extends Notifier { void build() { _uploadService = ref.watch(uploadServiceProvider); _service = ref.watch(actionServiceProvider); + _assetService = ref.watch(assetServiceProvider); _downloadService = ref.watch(downloadServiceProvider); _downloadService.onImageDownloadStatus = _downloadImageCallback; _downloadService.onVideoDownloadStatus = _downloadVideoCallback; @@ -115,6 +122,16 @@ class ActionNotifier extends Notifier { }; } + Future troubleshoot(ActionSource source, BuildContext context) async { + final assets = _getAssets(source); + if (assets.length > 1) { + return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets'); + } + context.pushRoute(AssetTroubleshootRoute(asset: assets.first)); + + return ActionResult(count: assets.length, success: true); + } + Future shareLink(ActionSource source, BuildContext context) async { final ids = _getRemoteIdsForSource(source); try { @@ -323,6 +340,14 @@ class ActionNotifier extends Notifier { final assets = _getOwnedRemoteAssetsForSource(source); try { await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); + if (source == ActionSource.viewer) { + final updatedParent = await _assetService.getRemoteAsset(assets.first.id); + if (updatedParent != null) { + ref.read(currentAssetNotifier.notifier).setAsset(updatedParent); + ref.read(assetViewerProvider.notifier).setAsset(updatedParent); + } + } + return ActionResult(count: assets.length, success: true); } catch (error, stack) { _logger.severe('Failed to unstack assets', error, stack); @@ -330,12 +355,12 @@ class ActionNotifier extends Notifier { } } - Future shareAssets(ActionSource source) async { + Future shareAssets(ActionSource source, BuildContext context) async { final ids = _getAssets(source).toList(growable: false); try { - final count = await _service.shareAssets(ids); - return ActionResult(count: count, success: true); + await _service.shareAssets(ids, context); + return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to share assets', error, stack); return ActionResult(count: ids.length, success: false, error: error.toString()); @@ -344,7 +369,6 @@ class ActionNotifier extends Notifier { Future downloadAll(ActionSource source) async { final assets = _getAssets(source).whereType().toList(growable: false); - try { final didEnqueue = await _service.downloadAll(assets); final enqueueCount = didEnqueue.where((e) => e).length; diff --git a/mobile/lib/providers/infrastructure/asset.provider.dart b/mobile/lib/providers/infrastructure/asset.provider.dart index 102e6aa60c..4b51ce33bd 100644 --- a/mobile/lib/providers/infrastructure/asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset.provider.dart @@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; final localAssetRepository = Provider( (ref) => DriftLocalAssetRepository(ref.watch(driftProvider)), @@ -19,9 +20,13 @@ final assetServiceProvider = Provider( ), ); -final placesProvider = FutureProvider>( - (ref) => AssetService( - remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider), - localAssetRepository: ref.watch(localAssetRepository), - ).getPlaces(), -); +final placesProvider = FutureProvider>((ref) { + final assetService = ref.watch(assetServiceProvider); + final auth = ref.watch(currentUserProvider); + + if (auth == null) { + return Future.value(const []); + } + + return assetService.getPlaces(auth.id); +}); diff --git a/mobile/lib/providers/infrastructure/memory.provider.dart b/mobile/lib/providers/infrastructure/memory.provider.dart index e5809a12b4..0965f4349b 100644 --- a/mobile/lib/providers/infrastructure/memory.provider.dart +++ b/mobile/lib/providers/infrastructure/memory.provider.dart @@ -14,13 +14,12 @@ final driftMemoryServiceProvider = Provider( (ref) => DriftMemoryService(ref.watch(driftMemoryRepositoryProvider)), ); -final driftMemoryFutureProvider = FutureProvider.autoDispose>((ref) async { - final user = ref.watch(currentUserProvider); - if (user == null) { - return []; +final driftMemoryFutureProvider = FutureProvider.autoDispose>((ref) { + final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true))); + if (userId == null || !enabled) { + return const []; } final service = ref.watch(driftMemoryServiceProvider); - - return service.getMemoryLane(user.id); + return service.getMemoryLane(userId); }); diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart index 6469624c09..11c5280c02 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -1,7 +1,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; +import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; +import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/thumbnail_api.g.dart'; +final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi())); + +final backgroundWorkerLockServiceProvider = Provider( + (_) => BackgroundWorkerLockService(BackgroundWorkerLockApi()), +); + final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); +final connectivityApiProvider = Provider((_) => ConnectivityApi()); + final thumbnailApi = ThumbnailApi(); diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart new file mode 100644 index 0000000000..9e96c3cfc4 --- /dev/null +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -0,0 +1,36 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +class ReadOnlyModeNotifier extends Notifier { + late AppSettingsService _appSettingService; + + @override + bool build() { + _appSettingService = ref.read(appSettingsServiceProvider); + final readonlyMode = _appSettingService.getSetting(AppSettingsEnum.readonlyModeEnabled); + return readonlyMode; + } + + void setMode(bool value) { + _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); + state = value; + + if (value) { + ref.read(appRouterProvider).navigate(const MainTimelineRoute()); + } + } + + void setReadonlyMode(bool isEnabled) { + state = isEnabled; + setMode(state); + } + + void toggleReadonlyMode() { + state = !state; + setMode(state); + } +} + +final readonlyModeProvider = NotifierProvider(() => ReadOnlyModeNotifier()); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index a48a1c30e4..38ba52dc56 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -12,43 +12,42 @@ import 'album.provider.dart'; class RemoteAlbumState { final List albums; - final List filteredAlbums; - const RemoteAlbumState({required this.albums, List? filteredAlbums}) - : filteredAlbums = filteredAlbums ?? albums; + const RemoteAlbumState({required this.albums}); - RemoteAlbumState copyWith({List? albums, List? filteredAlbums}) { - return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums); + RemoteAlbumState copyWith({List? albums}) { + return RemoteAlbumState(albums: albums ?? this.albums); } @override - String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})'; + String toString() => 'RemoteAlbumState(albums: ${albums.length})'; @override bool operator ==(covariant RemoteAlbumState other) { if (identical(this, other)) return true; final listEquals = const DeepCollectionEquality().equals; - return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums); + return listEquals(other.albums, albums); } @override - int get hashCode => albums.hashCode ^ filteredAlbums.hashCode; + int get hashCode => albums.hashCode; } class RemoteAlbumNotifier extends Notifier { late RemoteAlbumService _remoteAlbumService; final _logger = Logger('RemoteAlbumNotifier'); + @override RemoteAlbumState build() { _remoteAlbumService = ref.read(remoteAlbumServiceProvider); - return const RemoteAlbumState(albums: [], filteredAlbums: []); + return const RemoteAlbumState(albums: []); } Future> _getAll() async { try { final albums = await _remoteAlbumService.getAll(); - state = state.copyWith(albums: albums, filteredAlbums: albums); + state = state.copyWith(albums: albums); return albums; } catch (error, stack) { _logger.severe('Failed to fetch albums', error, stack); @@ -60,19 +59,21 @@ class RemoteAlbumNotifier extends Notifier { await _getAll(); } - void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) { - final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode); - - state = state.copyWith(filteredAlbums: filtered); + List searchAlbums( + List albums, + String query, + String? userId, [ + QuickFilterMode filterMode = QuickFilterMode.all, + ]) { + return _remoteAlbumService.searchAlbums(albums, query, userId, filterMode); } - void clearSearch() { - state = state.copyWith(filteredAlbums: state.albums); - } - - Future sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async { - final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); - state = state.copyWith(filteredAlbums: sortedAlbums); + Future> sortAlbums( + List albums, + RemoteAlbumSortMode sortMode, { + bool isReverse = false, + }) async { + return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse); } Future createAlbum({ @@ -83,7 +84,7 @@ class RemoteAlbumNotifier extends Notifier { try { final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds); - state = state.copyWith(albums: [...state.albums, album], filteredAlbums: [...state.filteredAlbums, album]); + state = state.copyWith(albums: [...state.albums, album]); return album; } catch (error, stack) { @@ -114,11 +115,7 @@ class RemoteAlbumNotifier extends Notifier { return album.id == albumId ? updatedAlbum : album; }).toList(); - final updatedFilteredAlbums = state.filteredAlbums.map((album) { - return album.id == albumId ? updatedAlbum : album; - }).toList(); - - state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums); + state = state.copyWith(albums: updatedAlbums); return updatedAlbum; } catch (error, stack) { @@ -139,9 +136,7 @@ class RemoteAlbumNotifier extends Notifier { await _remoteAlbumService.deleteAlbum(albumId); final updatedAlbums = state.albums.where((album) => album.id != albumId).toList(); - final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList(); - - state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums); + state = state.copyWith(albums: updatedAlbums); } Future> getAssets(String albumId) { @@ -164,9 +159,7 @@ class RemoteAlbumNotifier extends Notifier { await _remoteAlbumService.removeUser(albumId, userId: userId); final updatedAlbums = state.albums.where((album) => album.id != albumId).toList(); - final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList(); - - state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums); + state = state.copyWith(albums: updatedAlbums); } Future setActivityStatus(String albumId, bool enabled) { diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index ddc6eed441..f03754505c 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/providers/infrastructure/asset.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/storage.provider.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( @@ -35,7 +34,6 @@ final hashServiceProvider = Provider( (ref) => HashService( localAlbumRepository: ref.watch(localAlbumRepository), localAssetRepository: ref.watch(localAssetRepository), - storageRepository: ref.watch(storageRepositoryProvider), nativeSyncApi: ref.watch(nativeSyncApiProvider), ), ); diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index 5f32e07578..1d5511f1ff 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -7,11 +7,12 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final immichThemeModeProvider = StateProvider((ref) { final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode); - debugPrint("Current themeMode $themeMode"); + dPrint(() => "Current themeMode $themeMode"); if (themeMode == ThemeMode.light.name) { return ThemeMode.light; @@ -26,12 +27,12 @@ final immichThemePresetProvider = StateProvider((ref) { final appSettingsProvider = ref.watch(appSettingsServiceProvider); final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); - debugPrint("Current theme preset $primaryColorPreset"); + dPrint(() => "Current theme preset $primaryColorPreset"); try { return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset); } catch (e) { - debugPrint("Theme preset $primaryColorPreset not found. Applying default preset."); + dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset."); appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName); return defaultColorPreset; } diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index e225e0c98d..6949413cd9 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -28,6 +28,8 @@ class MultiSelectState { bool get hasRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged); + bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null); + bool get hasLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged); diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index e9e467346b..5aa924ed1c 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; enum UploadProfileStatus { idle, loading, success, failure } @@ -67,7 +67,7 @@ class UploadProfileImageNotifier extends StateNotifier var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes()); if (profileImagePath != null) { - debugPrint("Successfully upload profile image"); + dPrint(() => "Successfully upload profile image"); state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath); return true; } diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index fdc21592b5..136c6049a7 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -12,7 +10,6 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -// import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -21,6 +18,7 @@ import 'package:immich_mobile/utils/debounce.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } @@ -106,7 +104,7 @@ class WebsocketNotifier extends StateNotifier { headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}"; } - debugPrint("Attempting to connect to websocket"); + dPrint(() => "Attempting to connect to websocket"); // Configure socket transports must be specified Socket socket = io( endpoint.origin, @@ -122,12 +120,12 @@ class WebsocketNotifier extends StateNotifier { ); socket.onConnect((_) { - debugPrint("Established Websocket Connection"); + dPrint(() => "Established Websocket Connection"); state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); }); socket.onDisconnect((_) { - debugPrint("Disconnect to Websocket Connection"); + dPrint(() => "Disconnect to Websocket Connection"); state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); }); @@ -151,13 +149,13 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { - debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); + dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); } } } void disconnect() { - debugPrint("Attempting to disconnect from websocket"); + dPrint(() => "Attempting to disconnect from websocket"); _batchedAssetUploadReady.clear(); @@ -201,7 +199,7 @@ class WebsocketNotifier extends StateNotifier { } void listenUploadEvent() { - debugPrint("Start listening to event on_upload_success"); + dPrint(() => "Start listening to event on_upload_success"); state.socket?.on('on_upload_success', _handleOnUploadSuccess); } @@ -322,8 +320,15 @@ class WebsocketNotifier extends StateNotifier { return; } + final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false); try { - unawaited(_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList())); + unawaited( + _ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) { + if (isSyncAlbumEnabled) { + _ref.read(backgroundSyncProvider).syncLinkedAlbum(); + } + }), + ); } catch (error) { _log.severe("Error processing batched AssetUploadReadyV1 events: $error"); } diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index a0af217f0c..28a4ad661a 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -1,17 +1,20 @@ import 'dart:io'; +import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:share_plus/share_plus.dart'; final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider))); @@ -68,8 +71,9 @@ class AssetMediaRepository { } // TODO: make this more efficient - Future shareAssets(List assets) async { + Future shareAssets(List assets, BuildContext context) async { final downloadedXFiles = []; + final tempFiles = []; for (var asset in assets) { final localId = (asset is LocalAsset) @@ -80,6 +84,9 @@ class AssetMediaRepository { if (localId != null) { File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile; downloadedXFiles.add(XFile(f!.path)); + if (CurrentPlatform.isIOS) { + tempFiles.add(f); + } } else if (asset is RemoteAsset) { final tempDir = await getTemporaryDirectory(); final name = asset.name; @@ -93,6 +100,7 @@ class AssetMediaRepository { await tempFile.writeAsBytes(res.bodyBytes); downloadedXFiles.add(XFile(tempFile.path)); + tempFiles.add(tempFile); } else { _log.warning("Asset type not supported for sharing: $asset"); continue; @@ -104,15 +112,22 @@ class AssetMediaRepository { return 0; } - final result = await Share.shareXFiles(downloadedXFiles); - - for (var file in downloadedXFiles) { - try { - await File(file.path).delete(); - } catch (e) { - _log.warning("Failed to delete temporary file: ${file.path}", e); + // we dont want to await the share result since the + // "preparing" dialog will not disappear until + final size = context.sizeData; + Share.shareXFiles( + downloadedXFiles, + sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), + ).then((result) async { + for (var file in tempFiles) { + try { + await file.delete(); + } catch (e) { + _log.warning("Failed to delete temporary file: ${file.path}", e); + } } - } - return result.status == ShareResultStatus.success ? downloadedXFiles.length : 0; + }); + + return downloadedXFiles.length; } } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 9d7748254d..ba978b0df0 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,6 +1,5 @@ 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'; @@ -10,6 +9,7 @@ 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/infrastructure/repositories/sync_stream.repository.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'; @@ -25,25 +25,7 @@ class AuthRepository extends DatabaseRepository { const AuthRepository(super.db, this._drift); Future clearLocalData() async { - // Drift deletions - child entities first (those with foreign keys) - await Future.wait([ - _drift.memoryAssetEntity.deleteAll(), - _drift.remoteAlbumAssetEntity.deleteAll(), - _drift.remoteAlbumUserEntity.deleteAll(), - _drift.remoteExifEntity.deleteAll(), - _drift.userMetadataEntity.deleteAll(), - _drift.partnerEntity.deleteAll(), - _drift.stackEntity.deleteAll(), - _drift.assetFaceEntity.deleteAll(), - ]); - // Drift deletions - parent entities - await Future.wait([ - _drift.memoryEntity.deleteAll(), - _drift.personEntity.deleteAll(), - _drift.remoteAlbumEntity.deleteAll(), - _drift.remoteAssetEntity.deleteAll(), - _drift.userEntity.deleteAll(), - ]); + await SyncStreamRepository(_drift).reset(); return db.writeTxn(() { return Future.wait([ diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart index c4b31e9d93..1ac9410fc6 100644 --- a/mobile/lib/repositories/download.repository.dart +++ b/mobile/lib/repositories/download.repository.dart @@ -90,7 +90,11 @@ class DownloadRepository { final isVideo = asset.isVideo; final url = getOriginalUrlForRemoteId(id); - if (Platform.isAndroid || livePhotoVideoId == null || isVideo) { + // on iOS it cannot link the image, check if the filename has .MP extension + // to avoid downloading the video part + final isAndroidMotionPhoto = asset.name.contains(".MP"); + + if (Platform.isAndroid || livePhotoVideoId == null || isVideo || isAndroidMotionPhoto) { tasks[taskIndex++] = DownloadTask( taskId: id, url: url, diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index 82554d62e8..d497da4d4c 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -22,14 +22,14 @@ class PartnerApiRepository extends ApiRepository { } Future create(String id) async { - final dto = await checkNull(_api.createPartner(id)); + final dto = await checkNull(_api.createPartnerDeprecated(id)); return UserConverter.fromPartnerDto(dto); } Future delete(String id) => _api.removePartner(id); Future update(String id, {required bool inTimeline}) async { - final dto = await checkNull(_api.updatePartner(id, UpdatePartnerDto(inTimeline: inTimeline))); + final dto = await checkNull(_api.updatePartner(id, PartnerUpdateDto(inTimeline: inTimeline))); return UserConverter.fromPartnerDto(dto); } } diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 220dbf81c3..38f2c22cf2 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -1,7 +1,21 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/material.dart'; +import 'package:cancellation_token_http/http.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/entities/store.entity.dart'; +import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; + +class UploadTaskWithFile { + final File file; + final UploadTask task; + + const UploadTaskWithFile({required this.file, required this.task}); +} final uploadRepositoryProvider = Provider((ref) => UploadRepository()); @@ -27,8 +41,12 @@ class UploadRepository { ); } - void enqueueBackgroundAll(List tasks) { - FileDownloader().enqueueAll(tasks); + Future enqueueBackground(UploadTask task) { + return FileDownloader().enqueue(task); + } + + Future> enqueueBackgroundAll(List tasks) { + return FileDownloader().enqueueAll(tasks); } Future deleteDatabaseRecords(String group) { @@ -61,13 +79,65 @@ class UploadRepository { FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup), ]); - debugPrint(""" + dPrint( + () => + """ Upload Info: Enqueued: ${enqueuedTasks.length} Running: ${runningTasks.length} Canceled: ${canceledTasks.length} Waiting: ${waitingTasks.length} Paused: ${pausedTasks.length} - """); + """, + ); + } + + Future backupWithDartClient(Iterable tasks, CancellationToken cancelToken) async { + final httpClient = Client(); + final String savedEndpoint = Store.get(StoreKey.serverEndpoint); + + Logger logger = Logger('UploadRepository'); + for (final candidate in tasks) { + if (cancelToken.isCancelled) { + logger.warning("Backup was cancelled by the user"); + break; + } + + try { + final fileStream = candidate.file.openRead(); + final assetRawUploadData = MultipartFile( + "assetData", + fileStream, + candidate.file.lengthSync(), + filename: candidate.task.filename, + ); + + final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets')); + + baseRequest.headers.addAll(candidate.task.headers); + baseRequest.fields.addAll(candidate.task.fields); + baseRequest.files.add(assetRawUploadData); + + final response = await httpClient.send(baseRequest, cancellationToken: cancelToken); + + final responseBody = jsonDecode(await response.stream.bytesToString()); + + if (![200, 201].contains(response.statusCode)) { + final error = responseBody; + + logger.warning( + "Error(${error['statusCode']}) uploading ${candidate.task.filename} | Created on ${candidate.task.fields["fileCreatedAt"]} | ${error['error']}", + ); + + continue; + } + } on CancelledException { + logger.warning("Backup was cancelled by the user"); + break; + } catch (error, stackTrace) { + logger.warning("Error backup asset: ${error.toString()}: $stackTrace"); + continue; + } + } } } diff --git a/mobile/lib/routing/duplicate_guard.dart b/mobile/lib/routing/duplicate_guard.dart index 6f83d5297e..c55c7318d0 100644 --- a/mobile/lib/routing/duplicate_guard.dart +++ b/mobile/lib/routing/duplicate_guard.dart @@ -1,5 +1,5 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; /// Guards against duplicate navigation to this route class DuplicateGuard extends AutoRouteGuard { @@ -8,7 +8,7 @@ class DuplicateGuard extends AutoRouteGuard { void onNavigation(NavigationResolver resolver, StackRouter router) async { // Duplicate navigation if (resolver.route.name == router.current.name) { - debugPrint('DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); + dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); resolver.next(false); } else { resolver.next(true); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b289cc3225..7554c7b1cf 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -76,16 +76,18 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; 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/settings/beta_sync_settings.page.dart'; +import 'package:immich_mobile/pages/settings/sync_status.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/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/download_info.page.dart'; import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart'; import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_asset_troubleshoot.page.dart'; import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; @@ -332,7 +334,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: BetaSyncSettingsRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]), AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), @@ -343,6 +345,8 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftFilterImageRoute.page), AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 84f2685ab5..4e488a30c7 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -403,6 +403,43 @@ class ArchiveRoute extends PageRouteInfo { ); } +/// generated route for +/// [AssetTroubleshootPage] +class AssetTroubleshootRoute extends PageRouteInfo { + AssetTroubleshootRoute({ + Key? key, + required BaseAsset asset, + List? children, + }) : super( + AssetTroubleshootRoute.name, + args: AssetTroubleshootRouteArgs(key: key, asset: asset), + initialChildren: children, + ); + + static const String name = 'AssetTroubleshootRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AssetTroubleshootPage(key: args.key, asset: args.asset); + }, + ); +} + +class AssetTroubleshootRouteArgs { + const AssetTroubleshootRouteArgs({this.key, required this.asset}); + + final Key? key; + + final BaseAsset asset; + + @override + String toString() { + return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}'; + } +} + /// generated route for /// [AssetViewerPage] class AssetViewerRoute extends PageRouteInfo { @@ -509,22 +546,6 @@ class BackupOptionsRoute extends PageRouteInfo { ); } -/// generated route for -/// [BetaSyncSettingsPage] -class BetaSyncSettingsRoute extends PageRouteInfo { - const BetaSyncSettingsRoute({List? children}) - : super(BetaSyncSettingsRoute.name, initialChildren: children); - - static const String name = 'BetaSyncSettingsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BetaSyncSettingsPage(); - }, - ); -} - /// generated route for /// [ChangeExperiencePage] class ChangeExperienceRoute extends PageRouteInfo { @@ -667,6 +688,22 @@ class CropImageRouteArgs { } } +/// generated route for +/// [DownloadInfoPage] +class DownloadInfoRoute extends PageRouteInfo { + const DownloadInfoRoute({List? children}) + : super(DownloadInfoRoute.name, initialChildren: children); + + static const String name = 'DownloadInfoRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DownloadInfoPage(); + }, + ); +} + /// generated route for /// [DriftActivitiesPage] class DriftActivitiesRoute extends PageRouteInfo { @@ -2629,6 +2666,22 @@ class SplashScreenRoute extends PageRouteInfo { ); } +/// generated route for +/// [SyncStatusPage] +class SyncStatusRoute extends PageRouteInfo { + const SyncStatusRoute({List? children}) + : super(SyncStatusRoute.name, initialChildren: children); + + static const String name = 'SyncStatusRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SyncStatusPage(); + }, + ); +} + /// generated route for /// [TabControllerPage] class TabControllerRoute extends PageRouteInfo { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 9a12745acd..9c3768080b 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -1,7 +1,6 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:auto_route/auto_route.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/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; @@ -11,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart'; @@ -199,14 +199,11 @@ class ActionService { } Future removeFromAlbum(List remoteIds, String albumId) async { - int removedCount = 0; final result = await _albumApiRepository.removeAssets(albumId, remoteIds); - if (result.removed.isNotEmpty) { - removedCount = await _remoteAlbumRepository.removeAssets(albumId, result.removed); + await _remoteAlbumRepository.removeAssets(albumId, result.removed); } - - return removedCount; + return result.removed.length; } Future updateDescription(String assetId, String description) async { @@ -227,8 +224,8 @@ class ActionService { await _assetApiRepository.unStack(stackIds); } - Future shareAssets(List assets) { - return _assetMediaRepository.shareAssets(assets); + Future shareAssets(List assets, BuildContext context) { + return _assetMediaRepository.shareAssets(assets, context); } Future> downloadAll(List assets) { diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 454f652035..a9eee0528e 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -3,7 +3,6 @@ import 'dart:collection'; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -24,6 +23,7 @@ import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final albumServiceProvider = Provider( (ref) => AlbumService( @@ -124,7 +124,7 @@ class AlbumService { } finally { _localCompleter.complete(changes); } - debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); + dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); return changes; } @@ -172,7 +172,7 @@ class AlbumService { } finally { _remoteCompleter.complete(changes); } - debugPrint("refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); + dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); return changes; } @@ -220,7 +220,7 @@ class AlbumService { return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); } catch (e) { - debugPrint("Error addAssets ${e.toString()}"); + dPrint(() => "Error addAssets ${e.toString()}"); } return null; } @@ -242,7 +242,7 @@ class AlbumService { await _albumRepository.update(album); return true; } catch (e) { - debugPrint("Error setActivityEnabled ${e.toString()}"); + dPrint(() => "Error setActivityEnabled ${e.toString()}"); } return false; } @@ -271,7 +271,7 @@ class AlbumService { } return true; } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); + dPrint(() => "Error deleteAlbum ${e.toString()}"); } return false; } @@ -281,7 +281,7 @@ class AlbumService { await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); return true; } catch (e) { - debugPrint("Error leaveAlbum ${e.toString()}"); + dPrint(() => "Error leaveAlbum ${e.toString()}"); return false; } } @@ -293,7 +293,7 @@ class AlbumService { await _updateAssets(album.id, remove: toRemove.toList()); return true; } catch (e) { - debugPrint("Error removeAssetFromAlbum ${e.toString()}"); + dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); } return false; } @@ -310,7 +310,7 @@ class AlbumService { return true; } catch (error) { - debugPrint("Error removeUser ${error.toString()}"); + dPrint(() => "Error removeUser ${error.toString()}"); return false; } } @@ -327,7 +327,7 @@ class AlbumService { return true; } catch (error) { - debugPrint("Error addUsers ${error.toString()}"); + dPrint(() => "Error addUsers ${error.toString()}"); } return false; } @@ -340,7 +340,7 @@ class AlbumService { await _albumRepository.update(album); return true; } catch (e) { - debugPrint("Error changeTitleAlbum ${e.toString()}"); + dPrint(() => "Error changeTitleAlbum ${e.toString()}"); return false; } } @@ -353,7 +353,7 @@ class AlbumService { await _albumRepository.update(album); return true; } catch (e) { - debugPrint("Error changeDescriptionAlbum ${e.toString()}"); + dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); return false; } } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index fca9080c86..4033ffb184 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -11,6 +10,7 @@ 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'; +import 'package:immich_mobile/utils/debug_print.dart'; class ApiService implements Authentication { late ApiClient _apiClient; @@ -155,7 +155,7 @@ class ApiService implements Authentication { return endpoint; } } catch (e) { - debugPrint("Could not locate /.well-known/immich at $baseUrl"); + dPrint(() => "Could not locate /.well-known/immich at $baseUrl"); } return ""; diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 8a4b0c6719..03d91328d1 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -46,10 +46,13 @@ enum AppSettingsEnum { syncAlbums(StoreKey.syncAlbums, null, false), autoEndpointSwitching(StoreKey.autoEndpointSwitching, null, false), photoManagerCustomFilter(StoreKey.photoManagerCustomFilter, null, true), - betaTimeline(StoreKey.betaTimeline, null, false), + betaTimeline(StoreKey.betaTimeline, null, true), enableBackup(StoreKey.enableBackup, null, false), useCellularForUploadVideos(StoreKey.useWifiForUploadVideos, null, false), - useCellularForUploadPhotos(StoreKey.useWifiForUploadPhotos, null, false); + useCellularForUploadPhotos(StoreKey.useWifiForUploadPhotos, null, false), + backupRequireCharging(StoreKey.backupRequireCharging, null, false), + backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30), + readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index ee61929c81..b9fab35442 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -1,7 +1,6 @@ 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/models/user.model.dart'; @@ -26,6 +25,7 @@ import 'package:immich_mobile/services/sync.service.dart'; import 'package:logging/logging.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final assetServiceProvider = Provider( (ref) => AssetService( @@ -87,7 +87,7 @@ class AssetService { getChangedAssets: _getRemoteAssetChanges, loadAssets: _getRemoteAssets, ); - debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); + dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); return changes; } @@ -156,7 +156,7 @@ class AssetService { if (a.isInDb) { await _assetRepository.transaction(() => _assetRepository.update(a)); } else { - debugPrint("[loadExif] parameter Asset is not from DB!"); + dPrint(() => "[loadExif] parameter Asset is not from DB!"); } } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index e6436df244..33a8e810f1 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -7,7 +7,6 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -29,6 +28,7 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:path_provider_foundation/path_provider_foundation.dart'; @@ -165,7 +165,7 @@ class BackgroundService { ]); } } catch (error) { - debugPrint("[_updateNotification] failed to communicate with plugin"); + dPrint(() => "[_updateNotification] failed to communicate with plugin"); } return false; } @@ -177,7 +177,7 @@ class BackgroundService { return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); } } catch (error) { - debugPrint("[_showErrorNotification] failed to communicate with plugin"); + dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); } return false; } @@ -188,7 +188,7 @@ class BackgroundService { return await _backgroundChannel.invokeMethod('clearErrorNotifications'); } } catch (error) { - debugPrint("[_clearErrorNotifications] failed to communicate with plugin"); + dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); } return false; } @@ -196,7 +196,7 @@ class BackgroundService { /// await to ensure this thread (foreground or background) has exclusive access Future acquireLock() async { if (_hasLock) { - debugPrint("WARNING: [acquireLock] called more than once"); + dPrint(() => "WARNING: [acquireLock] called more than once"); return true; } final int lockTime = Timeline.now; @@ -302,19 +302,19 @@ class BackgroundService { final bool hasAccess = await waitForLock; if (!hasAccess) { - debugPrint("[_callHandler] could not acquire lock, exiting"); + dPrint(() => "[_callHandler] could not acquire lock, exiting"); return false; } final translationsOk = await loadTranslations(); if (!translationsOk) { - debugPrint("[_callHandler] could not load translations"); + dPrint(() => "[_callHandler] could not load translations"); } final bool ok = await _onAssetsChanged(); return ok; } catch (error) { - debugPrint(error.toString()); + dPrint(() => error.toString()); return false; } finally { releaseLock(); @@ -324,14 +324,14 @@ class BackgroundService { _cancellationToken?.cancel(); return true; default: - debugPrint("Unknown method ${call.method}"); + dPrint(() => "Unknown method ${call.method}"); return false; } } Future _onAssetsChanged() async { final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); final ref = ProviderContainer( overrides: [ @@ -344,9 +344,7 @@ class BackgroundService { HttpSSLOptions.apply(); ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - if (kDebugMode) { - debugPrint("[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); - } + dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 3e29222b4c..539fd1fbd9 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart' as http; import 'package:collection/collection.dart'; -import 'package:flutter/material.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'; @@ -29,6 +28,7 @@ import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; import 'package:permission_handler/permission_handler.dart' as pm; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final backupServiceProvider = Provider( (ref) => BackupService( @@ -69,7 +69,7 @@ class BackupService { try { return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); } catch (e) { - debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); + dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); return null; } } @@ -356,8 +356,9 @@ class BackupService { final error = responseBody; final errorMessage = error['message'] ?? error['error']; - debugPrint( - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", + dPrint( + () => + "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", ); onError( @@ -398,11 +399,11 @@ class BackupService { } } } on http.CancelledException { - debugPrint("Backup was cancelled by the user"); + dPrint(() => "Backup was cancelled by the user"); anyErrors = true; break; } catch (error, stackTrace) { - debugPrint("Error backup asset: ${error.toString()}: $stackTrace"); + dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); anyErrors = true; continue; } finally { @@ -411,7 +412,7 @@ class BackupService { await file?.delete(); await livePhotoFile?.delete(); } catch (e) { - debugPrint("ERROR deleting file: ${e.toString()}"); + dPrint(() => "ERROR deleting file: ${e.toString()}"); } } } @@ -454,7 +455,9 @@ class BackupService { if (![200, 201].contains(response.statusCode)) { var error = responseBody; - debugPrint("Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}"); + dPrint( + () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", + ); } return responseBody.containsKey('id') ? responseBody['id'] : null; diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 1b717a6eeb..6226781919 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -1,9 +1,11 @@ import 'package:auto_route/auto_route.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service; import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.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/infrastructure/asset.provider.dart' as beta_asset_provider; @@ -16,7 +18,6 @@ import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/services/memory.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( @@ -66,19 +67,19 @@ class DeepLinkService { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default - if (isColdStart) (Store.isBetaTimelineEnabled) ? const MainTimelineRoute() : const PhotosRoute(), + if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), route, ]); } - Future handleScheme(PlatformDeepLink link, bool isColdStart) async { + Future handleScheme(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async { // get everything after the scheme, since Uri cannot parse path final intent = link.uri.host; final queryParams = link.uri.queryParameters; PageRouteInfo? deepLinkRoute = switch (intent) { "memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''), - "asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''), + "asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref), "album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''), _ => null, }; @@ -95,7 +96,7 @@ class DeepLinkService { return _handleColdStart(deepLinkRoute, isColdStart); } - Future handleMyImmichApp(PlatformDeepLink link, bool isColdStart) async { + Future handleMyImmichApp(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async { final path = link.uri.path; const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; @@ -105,7 +106,7 @@ class DeepLinkService { PageRouteInfo? deepLinkRoute; if (assetRegex.hasMatch(path)) { final assetId = assetRegex.firstMatch(path)?.group(1) ?? ''; - deepLinkRoute = await _buildAssetDeepLink(assetId); + deepLinkRoute = await _buildAssetDeepLink(assetId, ref); } else if (albumRegex.hasMatch(path)) { final albumId = albumRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildAlbumDeepLink(albumId); @@ -141,13 +142,14 @@ class DeepLinkService { } } - Future _buildAssetDeepLink(String assetId) async { + Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { if (Store.isBetaTimelineEnabled) { final asset = await _betaAssetService.getRemoteAsset(assetId); if (asset == null) { return null; } + AssetViewer.setAsset(ref, asset); return AssetViewerRoute(initialIndex: 0, timelineService: _betaTimelineFactory.fromAssets([asset])); } else { // TODO: Remove this when beta is default diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart index 48302be79c..9d1f4e51e8 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -16,9 +16,10 @@ class HashService { required IsarDeviceAssetRepository deviceAssetRepository, required BackgroundService backgroundService, this.batchSizeLimit = kBatchHashSizeLimit, - this.batchFileLimit = kBatchHashFileLimit, + int? batchFileLimit, }) : _deviceAssetRepository = deviceAssetRepository, - _backgroundService = backgroundService; + _backgroundService = backgroundService, + batchFileLimit = batchFileLimit ?? kBatchHashFileLimit; final IsarDeviceAssetRepository _deviceAssetRepository; final BackgroundService _backgroundService; diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart index e7fc3292e2..bf85f4a9a9 100644 --- a/mobile/lib/services/local_notification.service.dart +++ b/mobile/lib/services/local_notification.service.dart @@ -1,9 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final localNotificationService = Provider( (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), @@ -110,7 +110,7 @@ class LocalNotificationService { switch (notificationResponse.actionId) { case cancelUploadActionID: { - debugPrint("User cancelled manual upload operation"); + dPrint(() => "User cancelled manual upload operation"); ref.read(manualUploadProvider.notifier).cancelBackup(); } } diff --git a/mobile/lib/services/localization.service.dart b/mobile/lib/services/localization.service.dart index 8bee710544..af63894249 100644 --- a/mobile/lib/services/localization.service.dart +++ b/mobile/lib/services/localization.service.dart @@ -2,9 +2,9 @@ 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'; +import 'package:immich_mobile/utils/debug_print.dart'; /// Workaround to manually load translations in another Isolate Future loadTranslations() async { @@ -17,7 +17,7 @@ Future loadTranslations() async { assetLoader: const CodegenLoader(), path: translationsPath, useOnlyLangCode: false, - onLoadError: (e) => debugPrint(e.toString()), + onLoadError: (e) => dPrint(() => e.toString()), fallbackLocale: locales.values.first, ); diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 250fb67d82..f33adf80f9 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; @@ -10,6 +9,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( (ref) => SearchService( @@ -43,7 +43,7 @@ class SearchService { model: model, ); } catch (e) { - debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}"); + dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}"); return []; } } diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index 4319d9dbae..460e135421 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; @@ -6,6 +5,7 @@ import 'package:immich_mobile/models/server_info/server_features.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final serverInfoServiceProvider = Provider((ref) => ServerInfoService(ref.watch(apiServiceProvider))); @@ -21,7 +21,7 @@ class ServerInfoService { return ServerDiskInfo.fromDto(dto); } } catch (e) { - debugPrint("Error [getDiskInfo] ${e.toString()}"); + dPrint(() => "Error [getDiskInfo] ${e.toString()}"); } return null; } @@ -33,7 +33,7 @@ class ServerInfoService { return ServerVersion.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerVersion] ${e.toString()}"); + dPrint(() => "Error [getServerVersion] ${e.toString()}"); } return null; } @@ -45,7 +45,7 @@ class ServerInfoService { return ServerFeatures.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerFeatures] ${e.toString()}"); + dPrint(() => "Error [getServerFeatures] ${e.toString()}"); } return null; } @@ -57,7 +57,7 @@ class ServerInfoService { return ServerConfig.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerConfig] ${e.toString()}"); + dPrint(() => "Error [getServerConfig] ${e.toString()}"); } return null; } diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart index c24b9fb7f8..88189c6bcd 100644 --- a/mobile/lib/services/stack.service.dart +++ b/mobile/lib/services/stack.service.dart @@ -1,10 +1,10 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class StackService { const StackService(this._api, this._assetRepository); @@ -16,7 +16,7 @@ class StackService { try { return _api.stacksApi.getStack(stackId); } catch (error) { - debugPrint("Error while fetching stack: $error"); + dPrint(() => "Error while fetching stack: $error"); } return null; } @@ -25,7 +25,7 @@ class StackService { try { return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); } catch (error) { - debugPrint("Error while creating stack: $error"); + dPrint(() => "Error while creating stack: $error"); } return null; } @@ -34,7 +34,7 @@ class StackService { try { return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); } catch (error) { - debugPrint("Error while updating stack children: $error"); + dPrint(() => "Error while updating stack children: $error"); } return null; } @@ -54,7 +54,7 @@ class StackService { } await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); } catch (error) { - debugPrint("Error while deleting stack: $error"); + dPrint(() => "Error while deleting stack: $error"); } } } diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 7b420413cf..1a5cb2a116 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -147,7 +147,9 @@ class SyncService { dbUsers, compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), both: (UserDto a, UserDto b) { - if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) || + if ((a.updatedAt == null && b.updatedAt != null) || + (a.updatedAt != null && b.updatedAt == null) || + (a.updatedAt != null && b.updatedAt != null && !a.updatedAt!.isAtSameMomentAs(b.updatedAt!)) || a.isPartnerSharedBy != b.isPartnerSharedBy || a.isPartnerSharedWith != b.isPartnerSharedWith || a.inTimeline != b.inTimeline) { diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index 9e5193c8cb..e8e98562f7 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -3,12 +3,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/material.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; @@ -19,6 +20,8 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; final uploadServiceProvider = Provider((ref) { @@ -51,6 +54,7 @@ class UploadService { final StorageRepository _storageRepository; final DriftLocalAssetRepository _localAssetRepository; final AppSettingsService _appSettingsService; + final Logger _logger = Logger('UploadService'); final StreamController _taskStatusController = StreamController.broadcast(); final StreamController _taskProgressController = StreamController.broadcast(); @@ -78,24 +82,16 @@ class UploadService { _taskProgressController.close(); } - void enqueueTasks(List tasks) { - _uploadRepository.enqueueBackgroundAll(tasks); + Future> enqueueTasks(List tasks) { + return _uploadRepository.enqueueBackgroundAll(tasks); } Future> getActiveTasks(String group) { return _uploadRepository.getActiveTasks(group); } - Future getBackupTotalCount() { - return _backupRepository.getTotalCount(); - } - - Future getBackupRemainderCount(String userId) { - return _backupRepository.getRemainderCount(userId); - } - - Future getBackupFinishedCount(String userId) { - return _backupRepository.getBackupCount(userId); + Future<({int total, int remainder, int processing})> getBackupCounts(String userId) { + return _backupRepository.getAllCounts(userId); } Future manualBackup(List localAssets) async { @@ -113,7 +109,7 @@ class UploadService { } if (tasks.isNotEmpty) { - enqueueTasks(tasks); + await enqueueTasks(tasks); } } @@ -138,7 +134,6 @@ class UploadService { } final batch = candidates.skip(i).take(batchSize).toList(); - List tasks = []; for (final asset in batch) { final task = await _getUploadTask(asset); @@ -149,13 +144,50 @@ class UploadService { if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { count += tasks.length; - enqueueTasks(tasks); + await enqueueTasks(tasks); onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length)); } } } + Future startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async { + await _storageRepository.clearCache(); + + shouldAbortQueuingTasks = false; + + final candidates = await _backupRepository.getCandidates(userId); + if (candidates.isEmpty) { + return; + } + + const batchSize = 100; + for (int i = 0; i < candidates.length; i += batchSize) { + if (shouldAbortQueuingTasks || token.isCancelled) { + break; + } + + final batch = candidates.skip(i).take(batchSize).toList(); + List tasks = []; + for (final asset in batch) { + final requireWifi = _shouldRequireWiFi(asset); + if (requireWifi && !hasWifi) { + _logger.warning('Skipping upload for ${asset.id} because it requires WiFi'); + continue; + } + + final task = await _getUploadTaskWithFile(asset); + if (task != null) { + tasks.add(task); + } + } + + if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { + await _uploadRepository.backupWithDartClient(tasks, token); + } + } + } + /// Cancel all ongoing uploads and reset the upload queue /// /// Return the number of left over tasks in the queue @@ -174,10 +206,20 @@ class UploadService { return _uploadRepository.start(); } - void _handleTaskStatusUpdate(TaskStatusUpdate update) { + void _handleTaskStatusUpdate(TaskStatusUpdate update) async { switch (update.status) { case TaskStatus.complete: _handleLivePhoto(update); + + if (CurrentPlatform.isIOS) { + try { + final path = await update.task.filePath(); + await File(path).delete(); + } catch (e) { + _logger.severe('Error deleting file path for iOS: $e'); + } + } + break; default: @@ -214,10 +256,46 @@ class UploadService { enqueueTasks([uploadTask]); } catch (error, stackTrace) { - debugPrint("Error handling live photo upload task: $error $stackTrace"); + dPrint(() => "Error handling live photo upload task: $error $stackTrace"); } } + Future _getUploadTaskWithFile(LocalAsset asset) async { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + return null; + } + + final file = await _storageRepository.getFileForAsset(asset.id); + if (file == null) { + return null; + } + + final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name; + + String metadata = UploadTaskMetadata( + localAssetId: asset.id, + isLivePhotos: entity.isLivePhoto, + livePhotoVideoId: '', + ).toJson(); + + return UploadTaskWithFile( + file: file, + task: await buildUploadTask( + file, + createdAt: asset.createdAt, + modifiedAt: asset.updatedAt, + originalFileName: originalFileName, + deviceAssetId: asset.id, + metadata: metadata, + group: "group", + priority: 0, + isFavorite: asset.isFavorite, + requiresWiFi: false, + ), + ); + } + Future _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { @@ -254,16 +332,12 @@ class UploadService { livePhotoVideoId: '', ).toJson(); - bool requiresWiFi = true; - - if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { - requiresWiFi = false; - } else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) { - requiresWiFi = false; - } + final requiresWiFi = _shouldRequireWiFi(asset); return buildUploadTask( file, + createdAt: asset.createdAt, + modifiedAt: asset.updatedAt, originalFileName: originalFileName, deviceAssetId: asset.id, metadata: metadata, @@ -287,20 +361,39 @@ class UploadService { final fields = {'livePhotoVideoId': livePhotoVideoId}; + final requiresWiFi = _shouldRequireWiFi(asset); + return buildUploadTask( file, + createdAt: asset.createdAt, + modifiedAt: asset.updatedAt, originalFileName: asset.name, deviceAssetId: asset.id, fields: fields, group: kBackupLivePhotoGroup, priority: 0, // Highest priority to get upload immediately isFavorite: asset.isFavorite, + requiresWiFi: requiresWiFi, ); } + bool _shouldRequireWiFi(LocalAsset asset) { + bool requiresWiFi = true; + + if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { + requiresWiFi = false; + } else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) { + requiresWiFi = false; + } + + return requiresWiFi; + } + Future buildUploadTask( File file, { required String group, + required DateTime createdAt, + required DateTime modifiedAt, Map? fields, String? originalFileName, String? deviceAssetId, @@ -314,15 +407,12 @@ class UploadService { final headers = ApiService.getRequestHeaders(); final deviceId = Store.get(StoreKey.deviceId); final (baseDirectory, directory, filename) = await Task.split(filePath: file.path); - final stats = await file.stat(); - final fileCreatedAt = stats.changed; - final fileModifiedAt = stats.modified; final fieldsMap = { 'filename': originalFileName ?? filename, 'deviceAssetId': deviceAssetId ?? '', 'deviceId': deviceId, - 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), - 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), + 'fileCreatedAt': createdAt.toUtc().toIso8601String(), + 'fileModifiedAt': modifiedAt.toUtc().toIso8601String(), 'isFavorite': isFavorite?.toString() ?? 'false', 'duration': '0', if (fields != null) ...fields, diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart index 99b949c9ac..d0cb8e646f 100644 --- a/mobile/lib/theme/dynamic_theme.dart +++ b/mobile/lib/theme/dynamic_theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; abstract final class DynamicTheme { const DynamicTheme._(); @@ -13,7 +14,7 @@ abstract final class DynamicTheme { final corePalette = await DynamicColorPlugin.getCorePalette(); if (corePalette != null) { final primaryColor = corePalette.toColorScheme().primary; - debugPrint('dynamic_color: Core palette detected.'); + dPrint(() => 'dynamic_color: Core palette detected.'); // Some palettes do not generate surface container colors accurately, // so we regenerate all colors using the primary color @@ -23,7 +24,7 @@ abstract final class DynamicTheme { ); } } catch (error) { - debugPrint('dynamic_color: Failed to obtain core palette: $error'); + dPrint(() => 'dynamic_color: Failed to obtain core palette: $error'); } } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 10facea9a2..c5a2583531 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -1,6 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; @@ -14,8 +16,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class ActionButtonContext { final BaseAsset asset; @@ -23,7 +25,9 @@ class ActionButtonContext { final bool isArchived; final bool isTrashEnabled; final bool isInLockedView; + final bool isStacked; final RemoteAlbum? currentAlbum; + final bool advancedTroubleshooting; final ActionSource source; const ActionButtonContext({ @@ -31,13 +35,16 @@ class ActionButtonContext { required this.isOwner, required this.isArchived, required this.isTrashEnabled, + required this.isStacked, required this.isInLockedView, required this.currentAlbum, + required this.advancedTroubleshooting, required this.source, }); } enum ActionButtonType { + advancedInfo, share, shareLink, archive, @@ -51,10 +58,12 @@ enum ActionButtonType { deleteLocal, upload, removeFromAlbum, + unstack, likeActivity; bool shouldShow(ActionButtonContext context) { return switch (this) { + ActionButtonType.advancedInfo => context.advancedTroubleshooting, ActionButtonType.share => true, ActionButtonType.shareLink => !context.isInLockedView && // @@ -97,7 +106,7 @@ enum ActionButtonType { context.asset.hasRemote, ActionButtonType.deleteLocal => !context.isInLockedView && // - context.asset.storage == AssetState.local, + context.asset.hasLocal, ActionButtonType.upload => !context.isInLockedView && // context.asset.storage == AssetState.local, @@ -105,6 +114,10 @@ enum ActionButtonType { context.isOwner && // !context.isInLockedView && // context.currentAlbum != null, + ActionButtonType.unstack => + context.isOwner && // + !context.isInLockedView && // + context.isStacked, ActionButtonType.likeActivity => !context.isInLockedView && context.currentAlbum != null && @@ -115,6 +128,7 @@ enum ActionButtonType { Widget buildButton(ActionButtonContext context) { return switch (this) { + ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source), ActionButtonType.share => ShareActionButton(source: context.source), ActionButtonType.shareLink => ShareLinkActionButton(source: context.source), ActionButtonType.archive => ArchiveActionButton(source: context.source), @@ -132,27 +146,13 @@ enum ActionButtonType { source: context.source, ), ActionButtonType.likeActivity => const LikeActivityActionButton(), + ActionButtonType.unstack => UnStackActionButton(source: context.source), }; } } class ActionButtonBuilder { - static const List _actionTypes = [ - ActionButtonType.share, - ActionButtonType.shareLink, - ActionButtonType.likeActivity, - ActionButtonType.archive, - ActionButtonType.unarchive, - ActionButtonType.download, - ActionButtonType.trash, - ActionButtonType.deletePermanent, - ActionButtonType.delete, - ActionButtonType.moveToLockFolder, - ActionButtonType.removeFromLockFolder, - ActionButtonType.deleteLocal, - ActionButtonType.upload, - ActionButtonType.removeFromAlbum, - ]; + static const List _actionTypes = ActionButtonType.values; static List build(ActionButtonContext context) { return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart new file mode 100644 index 0000000000..02142b1571 --- /dev/null +++ b/mobile/lib/utils/album_filter.utils.dart @@ -0,0 +1,25 @@ +import 'package:immich_mobile/domain/services/remote_album.service.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; + +class AlbumFilter { + String? userId; + String? query; + QuickFilterMode mode; + + AlbumFilter({required this.mode, this.userId, this.query}); + + AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) { + return AlbumFilter(userId: userId ?? this.userId, query: query ?? this.query, mode: mode ?? this.mode); + } +} + +class AlbumSort { + RemoteAlbumSortMode mode; + bool isReverse; + + AlbumSort({required this.mode, this.isReverse = false}); + + AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) { + return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse); + } +} diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 480d918b4e..c77ceaa62d 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,6 +1,8 @@ import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; @@ -11,6 +13,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; @@ -22,6 +25,36 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; +void configureFileDownloaderNotifications() { + FileDownloader().configureNotificationForGroup( + kDownloadGroupImage, + running: TaskNotification('downloading_media'.t(), '${'file_name'.t()}: {filename}'), + complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'), + progressBar: true, + ); + + FileDownloader().configureNotificationForGroup( + kDownloadGroupVideo, + running: TaskNotification('downloading_media'.t(), '${'file_name'.t()}: {filename}'), + complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'), + progressBar: true, + ); + + FileDownloader().configureNotificationForGroup( + kManualUploadGroup, + running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()), + complete: TaskNotification('upload_finished'.t(), 'backup_background_service_in_progress_notification'.t()), + groupNotificationId: kManualUploadGroup, + ); + + FileDownloader().configureNotificationForGroup( + kBackupGroup, + running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()), + complete: TaskNotification('upload_finished'.t(), 'backup_background_service_in_progress_notification'.t()), + groupNotificationId: kBackupGroup, + ); +} + abstract final class Bootstrap { static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { final drift = Drift(); @@ -56,11 +89,17 @@ abstract final class Bootstrap { return (isar, drift, logDb); } - static Future initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async { - final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? false; + static Future initDomain( + Isar db, + Drift drift, + DriftLogger logDb, { + bool listenStoreUpdates = true, + bool shouldBufferLogs = true, + }) async { + final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); - await StoreService.init(storeRepository: storeRepo); + await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); await LogService.init( logRepository: LogRepository(logDb), diff --git a/mobile/lib/utils/database.utils.dart b/mobile/lib/utils/database.utils.dart deleted file mode 100644 index 446b92db19..0000000000 --- a/mobile/lib/utils/database.utils.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; - -extension LocalAlbumEntityDataHelper on LocalAlbumEntityData { - LocalAlbum toDto({int assetCount = 0}) { - return LocalAlbum( - id: id, - name: name, - updatedAt: updatedAt, - assetCount: assetCount, - backupSelection: backupSelection, - ); - } -} - -extension LocalAssetEntityDataHelper 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/utils/datetime_helpers.dart b/mobile/lib/utils/datetime_helpers.dart new file mode 100644 index 0000000000..c13c8ca312 --- /dev/null +++ b/mobile/lib/utils/datetime_helpers.dart @@ -0,0 +1,19 @@ +const int _maxMillisecondsSinceEpoch = 8640000000000000; // 275760-09-13 +const int _minMillisecondsSinceEpoch = -62135596800000; // 0001-01-01 + +DateTime? tryFromSecondsSinceEpoch(int? secondsSinceEpoch, {bool isUtc = false}) { + if (secondsSinceEpoch == null) { + return null; + } + + final milliSeconds = secondsSinceEpoch * 1000; + if (milliSeconds < _minMillisecondsSinceEpoch || milliSeconds > _maxMillisecondsSinceEpoch) { + return null; + } + + try { + return DateTime.fromMillisecondsSinceEpoch(milliSeconds, isUtc: isUtc); + } catch (e) { + return null; + } +} diff --git a/mobile/lib/utils/debug_print.dart b/mobile/lib/utils/debug_print.dart new file mode 100644 index 0000000000..21f55fc6a5 --- /dev/null +++ b/mobile/lib/utils/debug_print.dart @@ -0,0 +1,8 @@ +import 'package:flutter/foundation.dart'; + +@pragma('vm:prefer-inline') +void dPrint(String Function() message) { + if (kDebugMode) { + debugPrint(message()); + } +} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index 58e7ad7f25..1ccf00d58b 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -1,14 +1,15 @@ import 'dart:async'; import 'dart:ui'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:logging/logging.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -31,55 +32,63 @@ Cancelable runInIsolateGentle({ } return workerManager.executeGentle((cancelledChecker) async { - BackgroundIsolateBinaryMessenger.ensureInitialized(token); - DartPluginRegistrant.ensureInitialized(); + T? result; + await runZonedGuarded( + () async { + BackgroundIsolateBinaryMessenger.ensureInitialized(token); + DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false); - final ref = ProviderContainer( - overrides: [ - // TODO: Remove once isar is removed - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - cancellationProvider.overrideWithValue(cancelledChecker), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); + final ref = ProviderContainer( + overrides: [ + // TODO: Remove once isar is removed + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + cancellationProvider.overrideWithValue(cancelledChecker), + driftProvider.overrideWith(driftOverride(drift)), + ], + ); - Logger log = Logger("IsolateLogger"); + Logger log = Logger("IsolateLogger"); - try { - HttpSSLOptions.apply(applyNative: false); - return await computation(ref); - } on CanceledError { - log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}"); - } catch (error, stack) { - log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack); - } finally { - try { - await LogService.I.flush(); - await logDb.close(); - await ref.read(driftProvider).close(); - - // Close Isar safely try { - final isar = ref.read(isarProvider); - if (isar.isOpen) { - await isar.close(); - } - } catch (e) { - debugPrint("Error closing Isar: $e"); - } + HttpSSLOptions.apply(applyNative: false); + result = await computation(ref); + } on CanceledError { + log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}"); + } catch (error, stack) { + log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack); + } finally { + try { + ref.dispose(); - ref.dispose(); - } catch (error) { - debugPrint("Error closing resources in isolate: $error"); - } finally { - ref.dispose(); - // Delay to ensure all resources are released - await Future.delayed(const Duration(seconds: 2)); - } - } - return null; + await Store.dispose(); + await LogService.I.dispose(); + await logDb.close(); + await drift.close(); + + // Close Isar safely + try { + if (isar.isOpen) { + await isar.close(); + } + } catch (e) { + dPrint(() => "Error closing Isar: $e"); + } + } catch (error, stack) { + dPrint(() => "Error closing resources in isolate: $error, $stack"); + } finally { + ref.dispose(); + // Delay to ensure all resources are released + await Future.delayed(const Duration(seconds: 2)); + } + } + }, + (error, stack) { + dPrint(() => "Error in isolate $debugLabel zone: $error, $stack"); + }, + ); + return result; }); } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 9816986b93..2ed6d9549f 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -4,8 +4,6 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -23,20 +21,19 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 14; +const int targetVersion = 17; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; final int version = Store.get(StoreKey.version, targetVersion); - if (version < 9) { await Store.put(StoreKey.version, targetVersion); final value = await db.storeValues.get(StoreKey.currentUser.id); @@ -66,6 +63,15 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.populateCache(); } + await handleBetaMigration(version, await _isNewInstallation(db, drift), SyncStreamRepository(drift)); + + if (version < 17 && Store.isBetaTimelineEnabled) { + final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); + if (delay >= 1000) { + await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); + } + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -78,6 +84,66 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } +Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { + // Handle migration only for this version + // TODO: remove when old timeline is removed + final isBeta = Store.tryGet(StoreKey.betaTimeline); + final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); + if (version <= 15 && needBetaMigration == null) { + // For new installations, no migration needed + // For existing installations, only migrate if beta timeline is not enabled (null or false) + if (isNewInstallation || isBeta == true) { + await Store.put(StoreKey.needBetaMigration, false); + await Store.put(StoreKey.betaTimeline, true); + } else { + await Store.put(StoreKey.needBetaMigration, true); + } + } + + if (version > 15) { + if (isBeta == null || isBeta) { + await Store.put(StoreKey.needBetaMigration, false); + await Store.put(StoreKey.betaTimeline, true); + } else { + await Store.put(StoreKey.needBetaMigration, false); + } + } + + if (version < 16) { + await syncStreamRepository.reset(); + await Store.put(StoreKey.shouldResetSync, true); + } +} + +Future _isNewInstallation(Isar db, Drift drift) async { + try { + final isarUserCount = await db.users.count(); + if (isarUserCount > 0) { + return false; + } + + final isarAssetCount = await db.assets.count(); + if (isarAssetCount > 0) { + return false; + } + + final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length); + if (driftStoreCount > 0) { + return false; + } + + final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length); + if (driftAssetCount > 0) { + return false; + } + + return true; + } catch (error) { + dPrint(() => "[MIGRATION] Error checking if new installation: $error"); + return false; + } +} + Future _migrateTo(Isar db, int version) async { await Store.delete(StoreKey.assetETag); await db.writeTxn(() async { @@ -99,10 +165,7 @@ Future _migrateDeviceAsset(Isar db) async { final PermissionState ps = await PhotoManager.requestPermissionExtend(); if (!ps.hasAccess) { - if (kDebugMode) { - debugPrint("[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - } - + dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); return; } @@ -122,8 +185,8 @@ Future _migrateDeviceAsset(Isar db) async { localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); } - debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}"); - debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}"); + dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}"); + dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}"); ids.sort((a, b) => a.assetId.compareTo(b.assetId)); localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); final List toAdd = []; @@ -138,20 +201,14 @@ Future _migrateDeviceAsset(Isar db) async { return false; }, onlyFirst: (deviceAsset) { - if (kDebugMode) { - debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - } + dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); }, onlySecond: (asset) { - if (kDebugMode) { - debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - } + dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); }, ); - if (kDebugMode) { - debugPrint("[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - } + dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); await db.writeTxn(() async { await db.deviceAssetEntitys.putAll(toAdd); @@ -171,7 +228,7 @@ Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { } }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating device assets to SQLite: $error"); + dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); } } @@ -219,7 +276,7 @@ Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { } }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating backup albums to SQLite: $error"); + dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); } } @@ -237,7 +294,7 @@ Future migrateStoreToSqlite(Isar db, Drift drift) async { } }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error"); + dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); } } @@ -252,7 +309,7 @@ Future migrateStoreToIsar(Isar db, Drift drift) async { await db.storeValues.putAll(driftStoreValues); }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating store values to Isar: $error"); + dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); } } @@ -263,16 +320,3 @@ class _DeviceAsset { const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); } - -Future> runNewSync(WidgetRef ref, {bool full = false}) { - ref.read(backupProvider.notifier).cancelBackup(); - - final backgroundManager = ref.read(backgroundSyncProvider); - return Future.wait([ - backgroundManager.syncLocal(full: full).then((_) { - Logger("runNewSync").fine("Hashing assets after syncLocal"); - return backgroundManager.hashAssets(); - }), - backgroundManager.syncRemote(), - ]); -} 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 b1e54af62a..cd2dc70dae 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -8,12 +8,14 @@ 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/widgets/album/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/widgets/common/drag_sheet.dart'; import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; final controlBottomAppBarNotifier = ControlBottomAppBarNotifier(); @@ -45,6 +47,7 @@ class ControlBottomAppBar extends HookConsumerWidget { final bool unfavorite; final bool unarchive; final AssetSelectionState selectionAssetState; + final List selectedAssets; const ControlBottomAppBar({ super.key, @@ -64,6 +67,7 @@ class ControlBottomAppBar extends HookConsumerWidget { this.onRemoveFromAlbum, this.onToggleLocked, this.selectionAssetState = const AssetSelectionState(), + this.selectedAssets = const [], this.enabled = true, this.unarchive = false, this.unfavorite = false, @@ -100,6 +104,18 @@ class ControlBottomAppBar extends HookConsumerWidget { ); } + /// Show existing AddToAlbumBottomSheet + void showAddToAlbumBottomSheet() { + showModalBottomSheet( + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), + context: context, + builder: (BuildContext _) { + return AddToAlbumBottomSheet(assets: selectedAssets); + }, + ); + } + void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) { if (!force) { deleteCb(force); @@ -121,6 +137,15 @@ class ControlBottomAppBar extends HookConsumerWidget { label: "share_link".tr(), onPressed: enabled ? () => onShare(false) : null, ), + if (!isInLockedView && hasRemote && albums.isNotEmpty) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 100), + child: ControlBoxButton( + iconData: Icons.photo_album, + label: "add_to_album".tr(), + onPressed: enabled ? showAddToAlbumBottomSheet : null, + ), + ), if (hasRemote && onArchive != null) ControlBoxButton( iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index c28f407e1e..da2957c027 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -440,6 +440,7 @@ class MultiselectGrid extends HookConsumerWidget { onUpload: onUpload, enabled: !processing.value, selectionAssetState: selectionAssetState.value, + selectedAssets: selection.value.toList(), onStack: stackEnabled ? onStack : null, onEditTime: editEnabled ? onEditTime : null, onEditLocation: editEnabled ? onEditLocation : null, diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index c7125e2200..00f7bc494d 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -98,7 +98,12 @@ class BottomGalleryBar extends ConsumerWidget { if (isDeleted) { // Can only trash assets stored in server. Local assets are always permanently removed for now if (context.mounted && asset.isRemote && isStackPrimaryAsset) { - ImmichToast.show(durationInSecond: 1, context: context, msg: 'Asset trashed', gravity: ToastGravity.BOTTOM); + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'asset_trashed'.tr(), + gravity: ToastGravity.BOTTOM, + ); } removeAssetFromStack(); } diff --git a/mobile/lib/widgets/asset_viewer/cast_dialog.dart b/mobile/lib/widgets/asset_viewer/cast_dialog.dart index 4db1d9bb69..f7c80cca3d 100644 --- a/mobile/lib/widgets/asset_viewer/cast_dialog.dart +++ b/mobile/lib/widgets/asset_viewer/cast_dialog.dart @@ -29,7 +29,7 @@ class CastDialog extends ConsumerWidget { future: ref.read(castProvider.notifier).getDevices(), builder: (context, snapshot) { if (snapshot.hasError) { - return Text('Error: ${snapshot.error.toString()}'); + return Text('error_saving_image'.tr(args: [snapshot.error.toString()])); } else if (!snapshot.hasData) { return const SizedBox(height: 48, child: Center(child: CircularProgressIndicator())); } diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index 04d01194e9..0edafa88c5 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; @@ -66,7 +67,7 @@ class ExifMap extends StatelessWidget { return; } - debugPrint('Opening Map Uri: $uri'); + dPrint(() => 'Opening Map Uri: $uri'); launchUrl(uri); }, onCreated: onMapCreated, diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index 6e34e89938..2ef7e24cd7 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -8,8 +8,17 @@ class BackupInfoCard extends StatelessWidget { final String title; final String subtitle; final String info; + final VoidCallback? onTap; - const BackupInfoCard({super.key, required this.title, required this.subtitle, required this.info, this.onTap}); + final bool isLoading; + const BackupInfoCard({ + super.key, + required this.title, + required this.subtitle, + required this.info, + this.onTap, + this.isLoading = false, + }); @override Widget build(BuildContext context) { @@ -38,8 +47,36 @@ class BackupInfoCard extends StatelessWidget { trailing: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(info, style: context.textTheme.titleLarge), - Text("backup_info_card_assets", style: context.textTheme.labelLarge).tr(), + Stack( + children: [ + Text( + info, + style: context.textTheme.titleLarge?.copyWith( + color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255), + ), + ), + if (isLoading) + Positioned.fill( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: context.colorScheme.onSurface.withAlpha(150), + ), + ), + ), + ), + ], + ), + Text( + "backup_info_card_assets", + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255), + ), + ).tr(), ], ), ), diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart index cc3485e6f9..596e46d934 100644 --- a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -4,12 +4,9 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DriftAlbumInfoListTile extends HookConsumerWidget { @@ -22,8 +19,6 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { final bool isSelected = album.backupSelection == BackupSelection.selected; final bool isExcluded = album.backupSelection == BackupSelection.excluded; - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - buildTileColor() { if (isSelected) { return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); @@ -75,9 +70,6 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } else { ref.read(backupAlbumProvider.notifier).selectAlbum(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } } }, leading: buildIcon(), 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 ccfc374fef..e504cf0675 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 @@ -1,7 +1,8 @@ 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:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:immich_mobile/entities/store.entity.dart'; 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'; @@ -12,6 +13,7 @@ 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/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -33,6 +35,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { final horizontalPadding = isHorizontal ? 100.0 : 20.0; final user = ref.watch(currentUserProvider); final isLoggingOut = useState(false); + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); useEffect(() { ref.read(backupProvider.notifier).updateDiskInfo(); @@ -214,6 +217,25 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } + buildReadonlyMessage() { + return Padding( + padding: const EdgeInsets.only(left: 10.0, right: 10.0), + child: ListTile( + dense: true, + visualDensity: VisualDensity.standard, + contentPadding: const EdgeInsets.only(left: 20, right: 20), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), + minLeadingWidth: 20, + tileColor: theme.primaryColor.withAlpha(80), + title: Text( + "profile_drawer_readonly_mode", + style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)), + textAlign: TextAlign.center, + ).tr(), + ), + ); + } + return Dismissible( behavior: HitTestBehavior.translucent, direction: DismissDirection.down, @@ -238,6 +260,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { const AppBarProfileInfoBox(), buildStorageInformation(), const AppBarServerInfo(), + if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildSettingButton(), buildSignOutButton(), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index b1f5b192dd..00366ca580 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -1,9 +1,12 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -17,6 +20,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider); final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status; + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final user = ref.watch(currentUserProvider); buildUserProfileImage() { @@ -55,6 +59,25 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } } + void toggleReadonlyMode() { + // read only mode is only supported int he beta experience + // TODO: remove this check when the beta UI goes stable + if (!Store.isBetaTimelineEnabled) return; + + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); + ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); + + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + ), + ), + ); + } + return Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Container( @@ -67,23 +90,25 @@ class AppBarProfileInfoBox extends HookConsumerWidget { minLeadingWidth: 50, leading: GestureDetector( onTap: pickUserProfileImage, + onLongPress: toggleReadonlyMode, child: Stack( clipBehavior: Clip.none, children: [ - buildUserProfileImage(), - Positioned( - bottom: -5, - right: -8, - child: Material( - color: context.colorScheme.surfaceContainerHighest, - elevation: 3, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), - child: Padding( - padding: const EdgeInsets.all(5.0), - child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14), + AbsorbPointer(child: buildUserProfileImage()), + if (!isReadonlyModeEnabled) + Positioned( + bottom: -5, + right: -8, + child: Material( + color: context.colorScheme.surfaceContainerHighest, + elevation: 3, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14), + ), ), ), - ), ], ), ), diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 7eaedd27b5..28b5c535d2 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -129,19 +129,24 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { title: Builder( builder: (BuildContext context) { return Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Builder( - builder: (context) { - return Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme - ? 'assets/immich-logo-inline-dark.svg' - : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ); - }, + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: SvgPicture.asset( + context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', + height: 40, + ), + ), + const Tooltip( + triggerMode: TooltipTriggerMode.tap, + showDuration: Duration(seconds: 4), + message: + "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.", + child: Padding( + padding: EdgeInsets.only(top: 3.0), + child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20), + ), ), ], ); diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 06a97d1ce5..23d64ecfcd 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; @@ -42,6 +43,7 @@ class ImmichSliverAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); return SliverAnimatedOpacity( @@ -57,7 +59,7 @@ class ImmichSliverAppBar extends ConsumerWidget { centerTitle: false, title: title ?? const _ImmichLogoWithText(), actions: [ - if (isCasting) + if (isCasting && !isReadonlyModeEnabled) Padding( padding: const EdgeInsets.only(right: 12), child: IconButton( @@ -70,12 +72,13 @@ class ImmichSliverAppBar extends ConsumerWidget { const _SyncStatusIndicator(), if (actions != null) ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (kDebugMode || kProfileMode) + if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled) IconButton( icon: const Icon(Icons.science_rounded), onPressed: () => context.pushRoute(const FeatInDevRoute()), ), - if (showUploadButton) const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()), + if (showUploadButton && !isReadonlyModeEnabled) + const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()), const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()), ], ), @@ -94,29 +97,11 @@ class _ImmichLogoWithText extends StatelessWidget { children: [ Builder( builder: (context) { - return Badge( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - backgroundColor: context.primaryColor, - alignment: Alignment.centerRight, - offset: const Offset(16, -8), - label: Text( - 'β', - style: TextStyle( - fontSize: 11, - color: context.colorScheme.onPrimary, - fontWeight: FontWeight.bold, - fontFamily: 'OverpassMono', - height: 1.2, - ), - ), - child: Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme - ? 'assets/immich-logo-inline-dark.svg' - : 'assets/immich-logo-inline-light.svg', - height: 40, - ), + return Padding( + padding: const EdgeInsets.only(top: 3.0), + child: SvgPicture.asset( + context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', + height: 40, ), ); }, @@ -137,8 +122,24 @@ class _ProfileIndicator extends ConsumerWidget { final user = ref.watch(currentUserProvider); const widgetSize = 30.0; + void toggleReadonlyMode() { + final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); + ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); + + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + ), + ), + ); + } + return InkWell( onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), + onLongPress: () => toggleReadonlyMode(), borderRadius: const BorderRadius.all(Radius.circular(12)), child: Badge( label: Container( @@ -154,7 +155,7 @@ class _ProfileIndicator extends ConsumerWidget { ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(radius: 17, size: 31, user: user), + child: AbsorbPointer(child: UserCircleAvatar(radius: 17, size: 31, user: user)), ), ), ); @@ -167,8 +168,16 @@ class _BackupIndicator extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { const widgetSize = 30.0; - final indicatorIcon = _getBackupBadgeIcon(context, ref); - final badgeBackground = context.colorScheme.surfaceContainer; + final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); + final indicatorIcon = hasError + ? Icon( + Icons.warning_rounded, + size: 12, + color: context.colorScheme.error, + semanticLabel: 'backup_controller_page_backup'.tr(), + ) + : _getBackupBadgeIcon(context, ref); + final badgeBackground = hasError ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer; return InkWell( onTap: () => context.pushRoute(const DriftBackupRoute()), @@ -277,7 +286,7 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with @override Widget build(BuildContext context) { final syncStatus = ref.watch(syncStatusProvider); - final isSyncing = syncStatus.isRemoteSyncing; + final isSyncing = syncStatus.isRemoteSyncing || syncStatus.isLocalSyncing; // Control animations based on sync status if (isSyncing) { diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 359b400456..73dbbfc85b 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -272,9 +272,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void initState() { super.initState(); - _zoomController = AnimationController(duration: const Duration(seconds: 12), vsync: this); + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + ); - _crossFadeController = AnimationController(duration: const Duration(milliseconds: 1200), vsync: this); + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + ); _zoomAnimation = Tween( begin: 1.0, diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index 1cc117139d..0f9555a101 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -378,9 +378,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void initState() { super.initState(); - _zoomController = AnimationController(duration: const Duration(seconds: 12), vsync: this); + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + ); - _crossFadeController = AnimationController(duration: const Duration(milliseconds: 1200), vsync: this); + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + ); _zoomAnimation = Tween( begin: 1.0, diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index 54497a10de..f75dd6e803 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -75,86 +75,79 @@ class _MesmerizingSliverAppBarState extends ConsumerState const SizedBox(height: 120), - _ => const SizedBox(height: 452), - }, - ); - } else { - return SliverAppBar( - expandedHeight: 400.0, - floating: false, - pinned: true, - snap: false, - elevation: 0, - leading: IconButton( - icon: Icon( - Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back, - color: actionIconColor, - shadows: actionIconShadows, - ), - onPressed: () => context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()])), - ), - actions: [ - if (widget.onToggleAlbumOrder != null) - IconButton( - icon: Icon(Icons.swap_vert_rounded, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onToggleAlbumOrder, - ), - if (currentAlbum.isActivityEnabled && currentAlbum.isShared) - IconButton( - icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onActivity, - ), - if (widget.onShowOptions != null) - IconButton( - icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onShowOptions, - ), - ], - title: Builder( - builder: (context) { - final settings = context.dependOnInheritedWidgetOfExactType(); - final scrollProgress = _calculateScrollProgress(settings); - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: scrollProgress > 0.95 - ? Text( - currentAlbum.name, - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18), - ) - : null, - ); - }, - ), - flexibleSpace: Builder( - builder: (context) { - final settings = context.dependOnInheritedWidgetOfExactType(); - final scrollProgress = _calculateScrollProgress(settings); - - // Update scroll progress for the leading button - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted && _scrollProgress != scrollProgress) { - setState(() { - _scrollProgress = scrollProgress; - }); - } - }); - - return FlexibleSpaceBar( - background: _ExpandedBackground( - scrollProgress: scrollProgress, - icon: widget.icon, - onEditTitle: widget.onEditTitle, + return SliverAppBar( + expandedHeight: 400.0, + floating: false, + pinned: true, + snap: false, + elevation: 0, + leading: isMultiSelectEnabled + ? const SizedBox.shrink() + : IconButton( + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back, + color: actionIconColor, + shadows: actionIconShadows, ), - ); - }, - ), - ); - } + onPressed: () => context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()])), + ), + actions: [ + if (widget.onToggleAlbumOrder != null) + IconButton( + icon: Icon(Icons.swap_vert_rounded, color: actionIconColor, shadows: actionIconShadows), + onPressed: widget.onToggleAlbumOrder, + ), + if (currentAlbum.isActivityEnabled && currentAlbum.isShared) + IconButton( + icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows), + onPressed: widget.onActivity, + ), + if (widget.onShowOptions != null) + IconButton( + icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows), + onPressed: widget.onShowOptions, + ), + ], + title: Builder( + builder: (context) { + final settings = context.dependOnInheritedWidgetOfExactType(); + final scrollProgress = _calculateScrollProgress(settings); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: scrollProgress > 0.95 + ? Text( + currentAlbum.name, + style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18), + ) + : null, + ); + }, + ), + flexibleSpace: Builder( + builder: (context) { + final settings = context.dependOnInheritedWidgetOfExactType(); + final scrollProgress = _calculateScrollProgress(settings); + + // Update scroll progress for the leading button + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _scrollProgress != scrollProgress) { + setState(() { + _scrollProgress = scrollProgress; + }); + } + }); + + return FlexibleSpaceBar( + background: _ExpandedBackground( + scrollProgress: scrollProgress, + icon: widget.icon, + onEditTitle: widget.onEditTitle, + ), + ); + }, + ), + ); } } @@ -378,9 +371,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void initState() { super.initState(); - _zoomController = AnimationController(duration: const Duration(seconds: 12), vsync: this); + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + ); - _crossFadeController = AnimationController(duration: const Duration(milliseconds: 1200), vsync: this); + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + ); _zoomAnimation = Tween( begin: 1.0, diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index ab78536a92..f100b58649 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -10,13 +10,16 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @@ -161,6 +164,18 @@ class LoginForm extends HookConsumerWidget { serverEndpointController.text = 'http://10.1.15.216:2283/api'; } + Future handleSyncFlow() async { + final backgroundManager = ref.read(backgroundSyncProvider); + + await backgroundManager.syncLocal(full: true); + await backgroundManager.syncRemote(); + await backgroundManager.hashAssets(); + + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); + } + } + login() async { TextInput.finishAutofillContext(); @@ -178,7 +193,8 @@ class LoginForm extends HookConsumerWidget { final isBeta = Store.isBetaTimelineEnabled; if (isBeta) { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - + handleSyncFlow(); + ref.read(websocketProvider.notifier).connect(); context.replaceRoute(const TabShellRoute()); return; } @@ -276,6 +292,7 @@ class LoginForm extends HookConsumerWidget { } if (isBeta) { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + handleSyncFlow(); context.replaceRoute(const TabShellRoute()); return; } diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart index 488b8480f2..b6c1e708a7 100644 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ b/mobile/lib/widgets/map/map_asset_grid.dart @@ -275,7 +275,7 @@ class _MapSheetDragRegion extends StatelessWidget { child: IconButton( icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color), iconSize: 24, - tooltip: 'Zoom to bounds', + tooltip: 'zoom_to_bounds'.tr(), onPressed: () => onZoomToAsset?.call(value!), ), ), diff --git a/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart b/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart index 51567eff15..e97875fd90 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart @@ -13,7 +13,7 @@ class MapSettingsListTile extends StatelessWidget { @override Widget build(BuildContext context) { return SwitchListTile.adaptive( - activeColor: context.primaryColor, + activeThumbColor: context.primaryColor, title: Text(title, style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(), value: selected, onChanged: onChanged, 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 b19c7dfdb6..d21b49f020 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 @@ -121,7 +121,6 @@ class PhotoViewCore extends StatefulWidget { class PhotoViewCoreState extends State with TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector { - Offset? _normalizedPosition; double? _scaleBefore; double? _rotationBefore; @@ -154,7 +153,6 @@ class PhotoViewCoreState extends State void onScaleStart(ScaleStartDetails details) { _rotationBefore = controller.rotation; _scaleBefore = scale; - _normalizedPosition = details.focalPoint - controller.position; _scaleAnimationController.stop(); _positionAnimationController.stop(); _rotationAnimationController.stop(); @@ -166,8 +164,14 @@ class PhotoViewCoreState extends State }; 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; - Offset delta = details.focalPoint - _normalizedPosition!; + final double scaleDelta = newScale / scale; + final Offset newPosition = + (controller.position + details.focalPointDelta) * scaleDelta - centeredFocalPoint * (scaleDelta - 1); updateScaleStateFromNewScale(newScale); @@ -176,7 +180,7 @@ class PhotoViewCoreState extends State updateMultiple( scale: newScale, - position: panEnabled ? delta : clampPosition(position: delta * details.scale), + position: panEnabled ? newPosition : clampPosition(position: newPosition), rotation: rotationEnabled ? _rotationBefore! + details.rotation : null, rotationFocusPoint: rotationEnabled ? details.focalPoint : null, ); @@ -346,8 +350,8 @@ class PhotoViewCoreState extends State final computedScale = useImageScale ? 1.0 : scale; final matrix = Matrix4.identity() - ..translate(value.position.dx, value.position.dy) - ..scale(computedScale) + ..translateByDouble(value.position.dx, value.position.dy, 0, 1.0) + ..scaleByDouble(computedScale, computedScale, computedScale, 1.0) ..rotateZ(value.rotation); final Widget customChildLayout = CustomSingleChildLayout( diff --git a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart index 65037dde96..a2ad04e6b5 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -172,12 +172,36 @@ class _ImageWrapperState extends State { @override Widget build(BuildContext context) { - if (_loading) { - return _buildLoading(context); - } - - if (_lastException != null) { - return _buildError(context); + if (_loading || _lastException != null) { + return CustomChildWrapper( + childSize: null, + backgroundDecoration: widget.backgroundDecoration, + heroAttributes: widget.heroAttributes, + scaleStateChangedCallback: widget.scaleStateChangedCallback, + enableRotation: widget.enableRotation, + controller: widget.controller, + scaleStateController: widget.scaleStateController, + maxScale: widget.maxScale, + minScale: widget.minScale, + initialScale: widget.initialScale, + basePosition: widget.basePosition, + scaleStateCycle: widget.scaleStateCycle, + onTapUp: widget.onTapUp, + onTapDown: widget.onTapDown, + onDragStart: widget.onDragStart, + onDragEnd: widget.onDragEnd, + onDragUpdate: widget.onDragUpdate, + onScaleEnd: widget.onScaleEnd, + onLongPressStart: widget.onLongPressStart, + outerSize: widget.outerSize, + gestureDetectorBehavior: widget.gestureDetectorBehavior, + tightMode: widget.tightMode, + filterQuality: widget.filterQuality, + disableGestures: widget.disableGestures, + disableScaleGestures: true, + enablePanAlways: widget.enablePanAlways, + child: _loading ? _buildLoading(context) : _buildError(context), + ); } final scaleBoundaries = ScaleBoundaries( diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index 589ce6262b..e0e34b654e 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -13,40 +13,19 @@ class MediaTypePicker extends HookWidget { Widget build(BuildContext context) { final selectedMediaType = useState(filter ?? AssetType.other); - return ListView( - shrinkWrap: true, - children: [ - RadioListTile( - key: const Key("all"), - title: const Text("all").tr(), - value: AssetType.other, - onChanged: (value) { - selectedMediaType.value = value!; - onSelect(value); - }, - groupValue: selectedMediaType.value, - ), - RadioListTile( - key: const Key("image"), - title: const Text("image").tr(), - value: AssetType.image, - onChanged: (value) { - selectedMediaType.value = value!; - onSelect(value); - }, - groupValue: selectedMediaType.value, - ), - RadioListTile( - key: const Key("video"), - title: const Text("video").tr(), - value: AssetType.video, - onChanged: (value) { - selectedMediaType.value = value!; - onSelect(value); - }, - groupValue: selectedMediaType.value, - ), - ], + return RadioGroup( + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + child: Column( + children: [ + RadioListTile(key: const Key("all"), title: const Text("all").tr(), value: AssetType.other), + RadioListTile(key: const Key("image"), title: const Text("image").tr(), value: AssetType.image), + RadioListTile(key: const Key("video"), title: const Text("video").tr(), value: AssetType.video), + ], + ), ); } } diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 3f196b840b..7a107b47d8 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,11 +6,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.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/utils/http_ssl_options.dart'; +import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -31,6 +35,7 @@ class AdvancedSettings extends HookConsumerWidget { final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert); final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); + final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; @@ -87,7 +92,7 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - const LocalStorageSettings(), + if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), SettingsSwitchListTile( enabled: !isLoggedIn, valueNotifier: allowSelfSignedSSLCert, @@ -97,11 +102,32 @@ class AdvancedSettings extends HookConsumerWidget { ), const CustomeProxyHeaderSettings(), SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null), - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), + if (!Store.isBetaTimelineEnabled) + SettingsSwitchListTile( + valueNotifier: useAlternatePMFilter, + title: "advanced_settings_enable_alternate_media_filter_title".tr(), + subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), + ), + const BetaTimelineListTile(), + if (Store.isBetaTimelineEnabled) + SettingsSwitchListTile( + valueNotifier: readonlyModeEnabled, + title: "advanced_settings_readonly_mode_title".tr(), + subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + onChanged: (value) { + readonlyModeEnabled.value = value; + ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + ), + ), + ); + }, + ), ]; return SettingsSubPageScaffold(settings: advancedSettings); diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 553eb939c2..743d38fc48 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -1,19 +1,247 @@ +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/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -class DriftBackupSettings extends StatelessWidget { +class DriftBackupSettings extends ConsumerWidget { const DriftBackupSettings({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SettingsSubPageScaffold( + settings: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + "network_requirements".t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)), + ), + ), + const _UseWifiForUploadVideosButton(), + const _UseWifiForUploadPhotosButton(), + if (CurrentPlatform.isAndroid) ...[ + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + "background_options".t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ), + const _BackupOnlyWhenChargingButton(), + const _BackupDelaySlider(), + ], + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + "backup_albums_sync".t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)), + ), + ), + const _AlbumSyncActionButton(), + ], + ); + } +} + +class _AlbumSyncActionButton extends ConsumerStatefulWidget { + const _AlbumSyncActionButton(); + + @override + ConsumerState<_AlbumSyncActionButton> createState() => _AlbumSyncActionButtonState(); +} + +class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> { + bool isAlbumSyncInProgress = false; + + Future _manualSyncAlbums() async { + setState(() { + isAlbumSyncInProgress = true; + }); + + try { + await ref.read(backgroundSyncProvider).syncLinkedAlbum(); + await ref.read(backgroundSyncProvider).syncRemote(); + } catch (_) { + } finally { + Future.delayed(const Duration(seconds: 1), () { + setState(() { + isAlbumSyncInProgress = false; + }); + }); + } + } + + Future _manageLinkedAlbums() async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + final localAlbums = ref.read(backupAlbumProvider); + final selectedBackupAlbums = localAlbums + .where((album) => album.backupSelection == BackupSelection.selected) + .toList(); + + await ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedBackupAlbums, currentUser.id); + } + @override Widget build(BuildContext context) { - return const SettingsSubPageScaffold(settings: [_UseWifiForUploadVideosButton(), _UseWifiForUploadPhotosButton()]); + return ListView( + shrinkWrap: true, + children: [ + StreamBuilder( + stream: Store.watch(StoreKey.syncAlbums), + initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, + builder: (context, snapshot) { + final albumSyncEnable = snapshot.data ?? false; + return Column( + children: [ + ListTile( + title: Text( + "sync_albums".t(context: context), + style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), + ), + subtitle: Text( + "sync_upload_album_setting_subtitle".t(context: context), + style: context.textTheme.labelLarge, + ), + trailing: Switch( + value: albumSyncEnable, + onChanged: (bool newValue) async { + await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue); + + if (newValue == true) { + await _manageLinkedAlbums(); + } + }, + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: albumSyncEnable ? 1.0 : 0.0, + child: albumSyncEnable + ? ListTile( + onTap: _manualSyncAlbums, + contentPadding: const EdgeInsets.only(left: 32, right: 16), + title: Text( + "organize_into_albums".t(context: context), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.normal, + ), + ), + subtitle: Text( + "organize_into_albums_description".t(context: context), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + trailing: isAlbumSyncInProgress + ? const SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ) + : IconButton( + onPressed: _manualSyncAlbums, + icon: const Icon(Icons.sync_rounded), + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + iconSize: 20, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + ) + : const SizedBox.shrink(), + ), + ), + ], + ); + }, + ), + ], + ); + } +} + +class _SettingsSwitchTile extends ConsumerStatefulWidget { + final AppSettingsEnum appSettingsEnum; + final String titleKey; + final String subtitleKey; + final void Function(bool?)? onChanged; + + const _SettingsSwitchTile({ + required this.appSettingsEnum, + required this.titleKey, + required this.subtitleKey, + this.onChanged, + }); + + @override + ConsumerState createState() => _SettingsSwitchTileState(); +} + +class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> { + late final Stream valueStream; + late final StreamSubscription subscription; + + @override + void initState() { + super.initState(); + valueStream = Store.watch(widget.appSettingsEnum.storeKey).asBroadcastStream(); + subscription = valueStream.listen((value) { + widget.onChanged?.call(value); + }); + } + + @override + void dispose() { + subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + widget.titleKey.t(context: context), + style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), + ), + subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge), + trailing: StreamBuilder( + stream: valueStream, + initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, + builder: (context, snapshot) { + final value = snapshot.data ?? false; + return Switch( + value: value, + onChanged: (bool newValue) async { + await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue); + }, + ); + }, + ), + ); } } @@ -22,29 +250,10 @@ class _UseWifiForUploadVideosButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final valueStream = Store.watch(StoreKey.useWifiForUploadVideos); - - return ListTile( - title: Text( - "videos".t(context: context), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ), - subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge), - trailing: StreamBuilder( - stream: valueStream, - initialData: Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false, - builder: (context, snapshot) { - final value = snapshot.data ?? false; - return Switch( - value: value, - onChanged: (bool newValue) async { - await ref - .read(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.useCellularForUploadVideos, newValue); - }, - ); - }, - ), + return const _SettingsSwitchTile( + appSettingsEnum: AppSettingsEnum.useCellularForUploadVideos, + titleKey: "videos", + subtitleKey: "network_requirement_videos_upload", ); } } @@ -54,29 +263,117 @@ class _UseWifiForUploadPhotosButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final valueStream = Store.watch(StoreKey.useWifiForUploadPhotos); - - return ListTile( - title: Text( - "photos".t(context: context), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ), - subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge), - trailing: StreamBuilder( - stream: valueStream, - initialData: Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false, - builder: (context, snapshot) { - final value = snapshot.data ?? false; - return Switch( - value: value, - onChanged: (bool newValue) async { - await ref - .read(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.useCellularForUploadPhotos, newValue); - }, - ); - }, - ), + return const _SettingsSwitchTile( + appSettingsEnum: AppSettingsEnum.useCellularForUploadPhotos, + titleKey: "photos", + subtitleKey: "network_requirement_photos_upload", + ); + } +} + +class _BackupOnlyWhenChargingButton extends ConsumerWidget { + const _BackupOnlyWhenChargingButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return _SettingsSwitchTile( + appSettingsEnum: AppSettingsEnum.backupRequireCharging, + titleKey: "charging", + subtitleKey: "charging_requirement_mobile_backup", + onChanged: (value) { + ref.read(backgroundWorkerFgServiceProvider).configure(requireCharging: value ?? false); + }, + ); + } +} + +class _BackupDelaySlider extends ConsumerStatefulWidget { + const _BackupDelaySlider(); + + @override + ConsumerState<_BackupDelaySlider> createState() => _BackupDelaySliderState(); +} + +class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> { + late final Stream valueStream; + late final StreamSubscription subscription; + late int currentValue; + + static int backupDelayToSliderValue(int ms) => switch (ms) { + 5 => 0, + 30 => 1, + 120 => 2, + _ => 3, + }; + + static int backupDelayToSeconds(int v) => switch (v) { + 0 => 5, + 1 => 30, + 2 => 120, + _ => 600, + }; + + static String formatBackupDelaySliderValue(int v) => switch (v) { + 0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}), + 1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}), + 2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}), + _ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}), + }; + + @override + void initState() { + super.initState(); + final initialValue = + Store.tryGet(AppSettingsEnum.backupTriggerDelay.storeKey) ?? AppSettingsEnum.backupTriggerDelay.defaultValue; + currentValue = backupDelayToSliderValue(initialValue); + + valueStream = Store.watch(AppSettingsEnum.backupTriggerDelay.storeKey).asBroadcastStream(); + subscription = valueStream.listen((value) { + if (mounted && value != null) { + setState(() { + currentValue = backupDelayToSliderValue(value); + }); + } + }); + } + + @override + void dispose() { + subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 8.0), + child: Text( + 'backup_controller_page_background_delay'.tr( + namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)}, + ), + style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), + ), + ), + Slider( + value: currentValue.toDouble(), + onChanged: (double v) { + setState(() { + currentValue = v.toInt(); + }); + }, + onChangeEnd: (double v) async { + final milliseconds = backupDelayToSeconds(v.toInt()); + await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.backupTriggerDelay, milliseconds); + }, + max: 3.0, + min: 0.0, + divisions: 3, + label: formatBackupDelaySliderValue(currentValue), + ), + ], ); } } diff --git a/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart b/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart deleted file mode 100644 index 8916fdd92b..0000000000 --- a/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart +++ /dev/null @@ -1,345 +0,0 @@ -import 'dart:io'; - -import 'package:drift/drift.dart' as drift_db; -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/translate_extensions.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; -import 'package:immich_mobile/providers/sync_status.provider.dart'; -import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -class BetaSyncSettings extends HookConsumerWidget { - const BetaSyncSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetService = ref.watch(assetServiceProvider); - final localAlbumService = ref.watch(localAlbumServiceProvider); - final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); - final memoryService = ref.watch(driftMemoryServiceProvider); - - Future> loadCounts() async { - final assetCounts = assetService.getAssetCounts(); - final localAlbumCounts = localAlbumService.getCount(); - final remoteAlbumCounts = remoteAlbumService.getCount(); - final memoryCount = memoryService.getCount(); - final getLocalHashedCount = assetService.getLocalHashedCount(); - - return await Future.wait([assetCounts, localAlbumCounts, remoteAlbumCounts, memoryCount, getLocalHashedCount]); - } - - Future resetDatabase() async { - // https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94 - final drift = ref.read(driftProvider); - final database = drift.attachedDatabase; - await database.exclusively(() async { - // https://stackoverflow.com/a/65743498/25690041 - await database.customStatement('PRAGMA writable_schema = 1;'); - await database.customStatement('DELETE FROM sqlite_master;'); - await database.customStatement('VACUUM;'); - await database.customStatement('PRAGMA writable_schema = 0;'); - await database.customStatement('PRAGMA integrity_check'); - - await database.customStatement('PRAGMA user_version = 0'); - await database.beforeOpen( - // ignore: invalid_use_of_internal_member - database.resolvedEngine.executor, - drift_db.OpeningDetails(null, database.schemaVersion), - ); - await database.customStatement('PRAGMA user_version = ${database.schemaVersion}'); - - // Refresh all stream queries - database.notifyUpdates({for (final table in database.allTables) drift_db.TableUpdate.onTable(table)}); - }); - } - - Future exportDatabase() async { - try { - // WAL Checkpoint to ensure all changes are written to the database - await ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"); - final documentsDir = await getApplicationDocumentsDirectory(); - final dbFile = File(path.join(documentsDir.path, 'immich.sqlite')); - - if (!await dbFile.exists()) { - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database file not found".t(context: context))), - ); - } - return; - } - - final timestamp = DateTime.now().millisecondsSinceEpoch; - final exportFile = File(path.join(documentsDir.path, 'immich_export_$timestamp.sqlite')); - - await dbFile.copy(exportFile.path); - - await Share.shareXFiles([XFile(exportFile.path)], text: 'Immich Database Export'); - - Future.delayed(const Duration(seconds: 30), () async { - if (await exportFile.exists()) { - await exportFile.delete(); - } - }); - - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database exported successfully".t(context: context))), - ); - } - } catch (e) { - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Failed to export database: $e".t(context: context))), - ); - } - } - } - - Future clearFileCache() async { - await ref.read(storageRepositoryProvider).clearCache(); - } - - return FutureBuilder>( - future: loadCounts(), - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const CircularProgressIndicator(); - } - - final assetCounts = snapshot.data![0]! as (int, int); - final localAssetCount = assetCounts.$1; - final remoteAssetCount = assetCounts.$2; - - final localAlbumCount = snapshot.data![1]! as int; - final remoteAlbumCount = snapshot.data![2]! as int; - final memoryCount = snapshot.data![3]! as int; - final localHashedCount = snapshot.data![4]! as int; - - return Padding( - padding: const EdgeInsets.only(top: 16, bottom: 32), - child: ListView( - children: [ - _SectionHeaderText(text: "assets".t(context: context)), - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Flex( - direction: Axis.horizontal, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, - children: [ - Expanded( - child: EntitiyCountTile( - label: "local".t(context: context), - count: localAssetCount, - icon: Icons.smartphone, - ), - ), - Expanded( - child: EntitiyCountTile( - label: "remote".t(context: context), - count: remoteAssetCount, - icon: Icons.cloud, - ), - ), - ], - ), - ), - _SectionHeaderText(text: "albums".t(context: context)), - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Flex( - direction: Axis.horizontal, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, - children: [ - Expanded( - child: EntitiyCountTile( - label: "local".t(context: context), - count: localAlbumCount, - icon: Icons.smartphone, - ), - ), - Expanded( - child: EntitiyCountTile( - label: "remote".t(context: context), - count: remoteAlbumCount, - icon: Icons.cloud, - ), - ), - ], - ), - ), - _SectionHeaderText(text: "other".t(context: context)), - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Flex( - direction: Axis.horizontal, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, - children: [ - Expanded( - child: EntitiyCountTile( - label: "memories".t(context: context), - count: memoryCount, - icon: Icons.calendar_today, - ), - ), - Expanded( - child: EntitiyCountTile( - label: "hashed_assets".t(context: context), - count: localHashedCount, - icon: Icons.tag, - ), - ), - ], - ), - ), - const Divider(height: 1, indent: 16, endIndent: 16), - const SizedBox(height: 24), - _SectionHeaderText(text: "jobs".t(context: context)), - ListTile( - title: Text( - "sync_local".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("tap_to_run_job".t(context: context)), - leading: const Icon(Icons.sync), - trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), - onTap: () { - ref.read(backgroundSyncProvider).syncLocal(full: true); - }, - ), - ListTile( - title: Text( - "sync_remote".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("tap_to_run_job".t(context: context)), - leading: const Icon(Icons.cloud_sync), - trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), - onTap: () { - ref.read(backgroundSyncProvider).syncRemote(); - }, - ), - ListTile( - title: Text( - "hash_asset".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - leading: const Icon(Icons.tag), - subtitle: Text("tap_to_run_job".t(context: context)), - trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), - onTap: () { - ref.read(backgroundSyncProvider).hashAssets(); - }, - ), - const Divider(height: 1, indent: 16, endIndent: 16), - const SizedBox(height: 24), - _SectionHeaderText(text: "actions".t(context: context)), - ListTile( - title: Text( - "clear_file_cache".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - leading: const Icon(Icons.playlist_remove_rounded), - onTap: clearFileCache, - ), - ListTile( - title: Text( - "export_database".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("export_database_description".t(context: context)), - leading: const Icon(Icons.download), - onTap: exportDatabase, - ), - ListTile( - title: Text( - "reset_sqlite".t(context: context), - style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500), - ), - leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error), - onTap: () async { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text("reset_sqlite".t(context: context)), - content: Text("reset_sqlite_confirmation".t(context: context)), - actions: [ - TextButton( - onPressed: () => context.pop(), - child: Text("cancel".t(context: context)), - ), - TextButton( - onPressed: () async { - await resetDatabase(); - context.pop(); - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("reset_sqlite_success".t(context: context))), - ); - }, - child: Text( - "confirm".t(context: context), - style: TextStyle(color: context.colorScheme.error), - ), - ), - ], - ); - }, - ); - }, - ), - ], - ), - ); - }, - ); - } -} - -class _SyncStatusIcon extends StatelessWidget { - final SyncStatus status; - - const _SyncStatusIcon({required this.status}); - - @override - Widget build(BuildContext context) { - return switch (status) { - SyncStatus.idle => const Icon(Icons.pause_circle_outline_rounded), - SyncStatus.syncing => const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2)), - SyncStatus.success => const Icon(Icons.check_circle_outline, color: Colors.green), - SyncStatus.error => Icon(Icons.error_outline, color: context.colorScheme.error), - }; - } -} - -class _SectionHeaderText extends StatelessWidget { - final String text; - - const _SectionHeaderText({required this.text}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - text.toUpperCase(), - style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(200), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart new file mode 100644 index 0000000000..a5bca24f81 --- /dev/null +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -0,0 +1,359 @@ +import 'dart:io'; + +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/translate_extensions.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; +import 'package:immich_mobile/providers/sync_status.provider.dart'; +import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +class SyncStatusAndActions extends HookConsumerWidget { + const SyncStatusAndActions({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Future exportDatabase() async { + try { + // WAL Checkpoint to ensure all changes are written to the database + await ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"); + final documentsDir = await getApplicationDocumentsDirectory(); + final dbFile = File(path.join(documentsDir.path, 'immich.sqlite')); + + if (!await dbFile.exists()) { + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Database file not found".t(context: context))), + ); + } + return; + } + + final timestamp = DateTime.now().millisecondsSinceEpoch; + final exportFile = File(path.join(documentsDir.path, 'immich_export_$timestamp.sqlite')); + + await dbFile.copy(exportFile.path); + + final size = MediaQuery.of(context).size; + await Share.shareXFiles( + [XFile(exportFile.path)], + text: 'Immich Database Export', + sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), + ); + + Future.delayed(const Duration(seconds: 30), () async { + if (await exportFile.exists()) { + await exportFile.delete(); + } + }); + + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Database exported successfully".t(context: context))), + ); + } + } catch (e) { + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Failed to export database: $e".t(context: context))), + ); + } + } + } + + Future clearFileCache() async { + await ref.read(storageRepositoryProvider).clearCache(); + } + + Future resetSqliteDb(BuildContext context) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text("reset_sqlite".t(context: context)), + content: Text("reset_sqlite_confirmation".t(context: context)), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text("cancel".t(context: context)), + ), + TextButton( + onPressed: () async { + await ref.read(driftProvider).reset(); + context.pop(); + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("reset_sqlite_success".t(context: context))), + ); + }, + child: Text( + "confirm".t(context: context), + style: TextStyle(color: context.colorScheme.error), + ), + ), + ], + ); + }, + ); + } + + return Padding( + padding: const EdgeInsets.only(top: 16, bottom: 32), + child: ListView( + children: [ + const _SyncStatsCounts(), + const Divider(height: 1, indent: 16, endIndent: 16), + const SizedBox(height: 24), + _SectionHeaderText(text: "jobs".t(context: context)), + ListTile( + title: Text( + "sync_local".t(context: context), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text("tap_to_run_job".t(context: context)), + leading: const Icon(Icons.sync), + trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), + onTap: () { + ref.read(backgroundSyncProvider).syncLocal(full: true); + }, + ), + ListTile( + title: Text( + "sync_remote".t(context: context), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text("tap_to_run_job".t(context: context)), + leading: const Icon(Icons.cloud_sync), + trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), + onTap: () { + ref.read(backgroundSyncProvider).syncRemote(); + }, + ), + ListTile( + title: Text( + "hash_asset".t(context: context), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + leading: const Icon(Icons.tag), + subtitle: Text("tap_to_run_job".t(context: context)), + trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), + onTap: () { + ref.read(backgroundSyncProvider).hashAssets(); + }, + ), + const Divider(height: 1, indent: 16, endIndent: 16), + const SizedBox(height: 24), + _SectionHeaderText(text: "actions".t(context: context)), + ListTile( + title: Text( + "clear_file_cache".t(context: context), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + leading: const Icon(Icons.playlist_remove_rounded), + onTap: clearFileCache, + ), + ListTile( + title: Text( + "export_database".t(context: context), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text("export_database_description".t(context: context)), + leading: const Icon(Icons.download), + onTap: exportDatabase, + ), + ListTile( + title: Text( + "reset_sqlite".t(context: context), + style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500), + ), + leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error), + onTap: () async { + await resetSqliteDb(context); + }, + ), + ], + ), + ); + } +} + +class _SyncStatusIcon extends StatelessWidget { + final SyncStatus status; + + const _SyncStatusIcon({required this.status}); + + @override + Widget build(BuildContext context) { + return switch (status) { + SyncStatus.idle => const Icon(Icons.pause_circle_outline_rounded), + SyncStatus.syncing => const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2)), + SyncStatus.success => const Icon(Icons.check_circle_outline, color: Colors.green), + SyncStatus.error => Icon(Icons.error_outline, color: context.colorScheme.error), + }; + } +} + +class _SectionHeaderText extends StatelessWidget { + final String text; + + const _SectionHeaderText({required this.text}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + text.toUpperCase(), + style: context.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withAlpha(200), + ), + ), + ); + } +} + +class _SyncStatsCounts extends ConsumerWidget { + const _SyncStatsCounts(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetService = ref.watch(assetServiceProvider); + final localAlbumService = ref.watch(localAlbumServiceProvider); + final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); + final memoryService = ref.watch(driftMemoryServiceProvider); + + Future> loadCounts() async { + final assetCounts = assetService.getAssetCounts(); + final localAlbumCounts = localAlbumService.getCount(); + final remoteAlbumCounts = remoteAlbumService.getCount(); + final memoryCount = memoryService.getCount(); + final getLocalHashedCount = assetService.getLocalHashedCount(); + + return await Future.wait([assetCounts, localAlbumCounts, remoteAlbumCounts, memoryCount, getLocalHashedCount]); + } + + return FutureBuilder( + future: loadCounts(), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: SizedBox(height: 48, width: 48, child: CircularProgressIndicator())); + } + + if (snapshot.hasError) { + return ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text( + "Error occur, reset the local database by tapping the button below", + style: context.textTheme.bodyLarge, + ), + ), + ), + ], + ); + } + + final assetCounts = snapshot.data![0]! as (int, int); + final localAssetCount = assetCounts.$1; + final remoteAssetCount = assetCounts.$2; + + final localAlbumCount = snapshot.data![1]! as int; + final remoteAlbumCount = snapshot.data![2]! as int; + final memoryCount = snapshot.data![3]! as int; + final localHashedCount = snapshot.data![4]! as int; + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionHeaderText(text: "assets".t(context: context)), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Expanded( + child: EntitiyCountTile( + label: "local".t(context: context), + count: localAssetCount, + icon: Icons.smartphone, + ), + ), + Expanded( + child: EntitiyCountTile( + label: "remote".t(context: context), + count: remoteAssetCount, + icon: Icons.cloud, + ), + ), + ], + ), + ), + _SectionHeaderText(text: "albums".t(context: context)), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Expanded( + child: EntitiyCountTile( + label: "local".t(context: context), + count: localAlbumCount, + icon: Icons.smartphone, + ), + ), + Expanded( + child: EntitiyCountTile( + label: "remote".t(context: context), + count: remoteAlbumCount, + icon: Icons.cloud, + ), + ), + ], + ), + ), + _SectionHeaderText(text: "other".t(context: context)), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Expanded( + child: EntitiyCountTile( + label: "memories".t(context: context), + count: memoryCount, + icon: Icons.calendar_today, + ), + ), + Expanded( + child: EntitiyCountTile( + label: "hashed_assets".t(context: context), + count: localHashedCount, + icon: Icons.tag, + ), + ), + ], + ), + ), + ], + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart index a498145275..1fefb3dcfa 100644 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -1,231 +1,68 @@ -import 'dart:math' as math; - import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.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/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -class BetaTimelineListTile extends ConsumerStatefulWidget { +class BetaTimelineListTile extends ConsumerWidget { const BetaTimelineListTile({super.key}); @override - ConsumerState createState() => _BetaTimelineListTileState(); -} - -class _BetaTimelineListTileState extends ConsumerState with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _rotationAnimation; - late Animation _pulseAnimation; - late Animation _gradientAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController(duration: const Duration(seconds: 3), vsync: this); - - _rotationAnimation = Tween( - begin: 0, - end: 2 * math.pi, - ).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear)); - - _pulseAnimation = Tween( - begin: 1, - end: 1.1, - ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - - _gradientAnimation = Tween( - begin: 0, - end: 1, - ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - - _animationController.repeat(reverse: true); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); - final serverInfo = ref.watch(serverInfoProvider); final auth = ref.watch(authProvider); - if (!auth.isAuthenticated || (serverInfo.serverVersion.minor < 136 && kReleaseMode)) { + if (!auth.isAuthenticated) { return const SizedBox.shrink(); } - return AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - void onSwitchChanged(bool value) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"), - content: value - ? const Text("Are you sure you want to enable the beta timeline?") - : const Text("Are you sure you want to disable the beta timeline?"), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: Text( - "cancel".t(context: context), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - final gradientColors = [ - Color.lerp( - context.primaryColor.withValues(alpha: 0.5), - context.primaryColor.withValues(alpha: 0.3), - _gradientAnimation.value, - )!, - Color.lerp( - context.logoPink.withValues(alpha: 0.2), - context.logoPink.withValues(alpha: 0.4), - _gradientAnimation.value, - )!, - Color.lerp( - context.logoRed.withValues(alpha: 0.3), - context.logoRed.withValues(alpha: 0.5), - _gradientAnimation.value, - )!, - ]; - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(12)), - gradient: LinearGradient( - colors: gradientColors, - stops: const [0.0, 0.5, 1.0], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: GradientRotation(_rotationAnimation.value * 0.5), - ), - boxShadow: [ - BoxShadow(color: context.primaryColor.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2)), - ], - ), - child: Container( - margin: const EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10.5)), - color: context.scaffoldBackgroundColor, - ), - child: Material( - borderRadius: const BorderRadius.all(Radius.circular(10.5)), - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(10.5)), - onTap: () => onSwitchChanged(!betaTimelineValue), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - Transform.scale( - scale: _pulseAnimation.value, - child: Transform.rotate( - angle: _rotationAnimation.value * 0.02, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [ - context.primaryColor.withValues(alpha: 0.2), - context.primaryColor.withValues(alpha: 0.1), - ], - ), - ), - child: Icon(Icons.auto_awesome, color: context.primaryColor, size: 20), - ), - ), - ), - const SizedBox(width: 28), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "advanced_settings_beta_timeline_title".t(context: context), - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - gradient: LinearGradient( - colors: [ - context.primaryColor.withValues(alpha: 0.8), - context.primaryColor.withValues(alpha: 0.6), - ], - ), - ), - child: Text( - 'NEW', - style: context.textTheme.labelSmall?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 10, - height: 1.2, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - "advanced_settings_beta_timeline_subtitle".t(context: context), - style: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withValues(alpha: 0.9), - ), - maxLines: 2, - ), - ], - ), - ), - Switch.adaptive( - value: betaTimelineValue, - onChanged: onSwitchChanged, - activeColor: context.primaryColor, - ), - ], - ), + void onSwitchChanged(bool value) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: value ? const Text("Enable New Timeline") : const Text("Disable New Timeline"), + content: value + ? const Text("Are you sure you want to enable the new timeline?") + : const Text("Are you sure you want to disable the new timeline?"), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + "cancel".t(context: context), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), ), ), - ), - ), - ); - }, + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]); + }, + child: Text("ok".t(context: context)), + ), + ], + ); + }, + ); + } + + return Padding( + padding: const EdgeInsets.only(left: 4.0), + child: ListTile( + title: Text("new_timeline".t(context: context)), + trailing: Switch.adaptive( + value: betaTimelineValue, + onChanged: onSwitchChanged, + activeThumbColor: context.primaryColor, + ), + onTap: () => onSwitchChanged(!betaTimelineValue), + ), ); } } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index ddf2cc6215..22c9154981 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -115,7 +115,7 @@ class PrimaryColorSetting extends HookConsumerWidget { child: SwitchListTile.adaptive( contentPadding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20), dense: true, - activeColor: context.primaryColor, + activeThumbColor: context.primaryColor, tileColor: context.colorScheme.surfaceContainerHigh, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15))), title: Text( diff --git a/mobile/lib/widgets/settings/settings_radio_list_tile.dart b/mobile/lib/widgets/settings/settings_radio_list_tile.dart index 95224e3f59..6b02a65159 100644 --- a/mobile/lib/widgets/settings/settings_radio_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_radio_list_tile.dart @@ -9,7 +9,7 @@ class SettingsRadioGroup { } class SettingsRadioListTile extends StatelessWidget { - final List groups; + final List> groups; final T groupBy; final void Function(T?) onRadioChanged; @@ -17,21 +17,23 @@ class SettingsRadioListTile extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: groups - .map( - (g) => RadioListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - dense: true, - activeColor: context.primaryColor, - title: Text(g.title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)), - value: g.value, - groupValue: groupBy, - onChanged: onRadioChanged, - controlAffinity: ListTileControlAffinity.trailing, - ), - ) - .toList(), + return RadioGroup( + groupValue: groupBy, + onChanged: onRadioChanged, + child: Column( + children: groups + .map( + (g) => RadioListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + dense: true, + activeColor: context.primaryColor, + title: Text(g.title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)), + value: g.value, + controlAffinity: ListTileControlAffinity.trailing, + ), + ) + .toList(), + ), ); } } diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index d51e2eb2ca..f5d6dfd05a 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -40,7 +40,7 @@ class SettingsSwitchListTile extends StatelessWidget { selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, onChanged: onSwitchChanged, - activeColor: enabled ? context.primaryColor : context.themeData.disabledColor, + activeThumbColor: enabled ? context.primaryColor : context.themeData.disabledColor, dense: true, secondary: icon != null ? Icon(icon!, color: valueNotifier.value ? context.primaryColor : null) : null, title: Text( diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index 0eced33ce3..cbd6e1f077 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class SharedLinkItem extends ConsumerWidget { final SharedLink sharedLink; @@ -36,7 +37,7 @@ class SharedLinkItem extends ConsumerWidget { return Text("expired", style: TextStyle(color: Colors.red[300])).tr(); } final difference = sharedLink.expiresAt!.difference(DateTime.now()); - debugPrint("Difference: $difference"); + dPrint(() => "Difference: $difference"); if (difference.inDays > 0) { var dayDifference = difference.inDays; if (difference.inHours % 24 > 12) { diff --git a/mobile/makefile b/mobile/makefile index 5a31481f45..b90e95c902 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -8,8 +8,14 @@ build: pigeon: dart run pigeon --input pigeon/native_sync_api.dart dart run pigeon --input pigeon/thumbnail_api.dart + dart run pigeon --input pigeon/background_worker_api.dart + dart run pigeon --input pigeon/background_worker_lock_api.dart + dart run pigeon --input pigeon/connectivity_api.dart dart format lib/platform/native_sync_api.g.dart dart format lib/platform/thumbnail_api.g.dart + dart format lib/platform/background_worker_api.g.dart + dart format lib/platform/background_worker_lock_api.g.dart + dart format lib/platform/connectivity_api.g.dart watch: dart run build_runner watch --delete-conflicting-outputs diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 04600250b1..47277a15bb 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.139.4 +- API version: 2.0.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -97,16 +97,20 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload *AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets +*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | getAllUserAssetsByDeviceId *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | +*AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | +*AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | -*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset +*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | +*AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | @@ -122,7 +126,9 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | +*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | +*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | @@ -167,7 +173,8 @@ Class | Method | HTTP request | Description *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | *OAuthApi* | [**startOAuth**](doc//OAuthApi.md#startoauth) | **POST** /oauth/authorize | *OAuthApi* | [**unlinkOAuthAccount**](doc//OAuthApi.md#unlinkoauthaccount) | **POST** /oauth/unlink | -*PartnersApi* | [**createPartner**](doc//PartnersApi.md#createpartner) | **POST** /partners/{id} | +*PartnersApi* | [**createPartner**](doc//PartnersApi.md#createpartner) | **POST** /partners | +*PartnersApi* | [**createPartnerDeprecated**](doc//PartnersApi.md#createpartnerdeprecated) | **POST** /partners/{id} | *PartnersApi* | [**getPartners**](doc//PartnersApi.md#getpartners) | **GET** /partners | *PartnersApi* | [**removePartner**](doc//PartnersApi.md#removepartner) | **DELETE** /partners/{id} | *PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} | @@ -328,6 +335,10 @@ Class | Method | HTTP request | Description - [AssetMediaResponseDto](doc//AssetMediaResponseDto.md) - [AssetMediaSize](doc//AssetMediaSize.md) - [AssetMediaStatus](doc//AssetMediaStatus.md) + - [AssetMetadataKey](doc//AssetMetadataKey.md) + - [AssetMetadataResponseDto](doc//AssetMetadataResponseDto.md) + - [AssetMetadataUpsertDto](doc//AssetMetadataUpsertDto.md) + - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) - [AssetOrder](doc//AssetOrder.md) - [AssetResponseDto](doc//AssetResponseDto.md) - [AssetStackResponseDto](doc//AssetStackResponseDto.md) @@ -382,6 +393,7 @@ Class | Method | HTTP request | Description - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) + - [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md) - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) @@ -408,8 +420,10 @@ Class | Method | HTTP request | Description - [OnThisDayDto](doc//OnThisDayDto.md) - [OnboardingDto](doc//OnboardingDto.md) - [OnboardingResponseDto](doc//OnboardingResponseDto.md) + - [PartnerCreateDto](doc//PartnerCreateDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) + - [PartnerUpdateDto](doc//PartnerUpdateDto.md) - [PeopleResponse](doc//PeopleResponse.md) - [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleUpdate](doc//PeopleUpdate.md) @@ -485,6 +499,8 @@ Class | Method | HTTP request | Description - [SyncAssetExifV1](doc//SyncAssetExifV1.md) - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) + - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) + - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) - [SyncAssetV1](doc//SyncAssetV1.md) - [SyncAuthUserV1](doc//SyncAuthUserV1.md) - [SyncEntityType](doc//SyncEntityType.md) @@ -556,7 +572,6 @@ Class | Method | HTTP request | Description - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - - [UpdatePartnerDto](doc//UpdatePartnerDto.md) - [UsageByUserDto](doc//UsageByUserDto.md) - [UserAdminCreateDto](doc//UserAdminCreateDto.md) - [UserAdminDeleteDto](doc//UserAdminDeleteDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index f5f353c968..df2c2226b1 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -106,6 +106,10 @@ part 'model/asset_jobs_dto.dart'; part 'model/asset_media_response_dto.dart'; part 'model/asset_media_size.dart'; part 'model/asset_media_status.dart'; +part 'model/asset_metadata_key.dart'; +part 'model/asset_metadata_response_dto.dart'; +part 'model/asset_metadata_upsert_dto.dart'; +part 'model/asset_metadata_upsert_item_dto.dart'; part 'model/asset_order.dart'; part 'model/asset_response_dto.dart'; part 'model/asset_stack_response_dto.dart'; @@ -160,6 +164,7 @@ part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; +part 'model/machine_learning_availability_checks_dto.dart'; part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; @@ -186,8 +191,10 @@ 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_create_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; +part 'model/partner_update_dto.dart'; part 'model/people_response.dart'; part 'model/people_response_dto.dart'; part 'model/people_update.dart'; @@ -263,6 +270,8 @@ part 'model/sync_asset_delete_v1.dart'; part 'model/sync_asset_exif_v1.dart'; part 'model/sync_asset_face_delete_v1.dart'; part 'model/sync_asset_face_v1.dart'; +part 'model/sync_asset_metadata_delete_v1.dart'; +part 'model/sync_asset_metadata_v1.dart'; part 'model/sync_asset_v1.dart'; part 'model/sync_auth_user_v1.dart'; part 'model/sync_entity_type.dart'; @@ -334,7 +343,6 @@ part 'model/update_album_dto.dart'; part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; -part 'model/update_partner_dto.dart'; part 'model/usage_by_user_dto.dart'; part 'model/user_admin_create_dto.dart'; part 'model/user_admin_delete_dto.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index c0de1a0801..063f9ea43b 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -18,7 +18,7 @@ class AssetsApi { /// checkBulkUpload /// - /// Checks if assets exist by checksums + /// Checks if assets exist by checksums. This endpoint requires the `asset.upload` permission. /// /// Note: This method returns the HTTP [Response]. /// @@ -52,7 +52,7 @@ class AssetsApi { /// checkBulkUpload /// - /// Checks if assets exist by checksums + /// Checks if assets exist by checksums. This endpoint requires the `asset.upload` permission. /// /// Parameters: /// @@ -128,6 +128,56 @@ class AssetsApi { return null; } + /// This endpoint requires the `asset.update` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetMetadataKey] key (required): + Future deleteAssetMetadataWithHttpInfo(String id, AssetMetadataKey key,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/metadata/{key}' + .replaceAll('{id}', id) + .replaceAll('{key}', key.toString()); + + // 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, + ); + } + + /// This endpoint requires the `asset.update` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetMetadataKey] key (required): + Future deleteAssetMetadata(String id, AssetMetadataKey key,) async { + final response = await deleteAssetMetadataWithHttpInfo(id, key,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// This endpoint requires the `asset.delete` permission. /// /// Note: This method returns the HTTP [Response]. @@ -368,6 +418,120 @@ class AssetsApi { return null; } + /// This endpoint requires the `asset.read` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + Future getAssetMetadataWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/metadata' + .replaceAll('{id}', id); + + // 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, + ); + } + + /// This endpoint requires the `asset.read` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + Future?> getAssetMetadata(String id,) async { + final response = await getAssetMetadataWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// This endpoint requires the `asset.read` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetMetadataKey] key (required): + Future getAssetMetadataByKeyWithHttpInfo(String id, AssetMetadataKey key,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/metadata/{key}' + .replaceAll('{id}', id) + .replaceAll('{key}', key.toString()); + + // 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, + ); + } + + /// This endpoint requires the `asset.read` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetMetadataKey] key (required): + Future getAssetMetadataByKey(String id, AssetMetadataKey key,) async { + final response = await getAssetMetadataByKeyWithHttpInfo(id, key,); + 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), 'AssetMetadataResponseDto',) as AssetMetadataResponseDto; + + } + return null; + } + /// This endpoint requires the `asset.statistics` permission. /// /// Note: This method returns the HTTP [Response]. @@ -565,9 +729,9 @@ class AssetsApi { return null; } - /// replaceAsset + /// Replace the asset with new file, without changing its id /// - /// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission. + /// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission. /// /// Note: This method returns the HTTP [Response]. /// @@ -659,9 +823,9 @@ class AssetsApi { ); } - /// replaceAsset + /// Replace the asset with new file, without changing its id /// - /// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission. + /// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission. /// /// Parameters: /// @@ -795,6 +959,66 @@ class AssetsApi { return null; } + /// This endpoint requires the `asset.update` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): + Future updateAssetMetadataWithHttpInfo(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/metadata' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = assetMetadataUpsertDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This endpoint requires the `asset.update` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): + Future?> updateAssetMetadata(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { + final response = await updateAssetMetadataWithHttpInfo(id, assetMetadataUpsertDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// This endpoint requires the `asset.update` permission. /// /// Note: This method returns the HTTP [Response]. @@ -855,6 +1079,8 @@ class AssetsApi { /// /// * [DateTime] fileModifiedAt (required): /// + /// * [List] metadata (required): + /// /// * [String] key: /// /// * [String] slug: @@ -873,7 +1099,7 @@ class AssetsApi { /// * [MultipartFile] sidecarData: /// /// * [AssetVisibility] visibility: - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -936,6 +1162,10 @@ class AssetsApi { hasFields = true; mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId); } + if (metadata != null) { + hasFields = true; + mp.fields[r'metadata'] = parameterToString(metadata); + } if (sidecarData != null) { hasFields = true; mp.fields[r'sidecarData'] = sidecarData.field; @@ -974,6 +1204,8 @@ class AssetsApi { /// /// * [DateTime] fileModifiedAt (required): /// + /// * [List] metadata (required): + /// /// * [String] key: /// /// * [String] slug: @@ -992,8 +1224,8 @@ class AssetsApi { /// * [MultipartFile] sidecarData: /// /// * [AssetVisibility] visibility: - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, metadata, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index f9a496b990..9246998ca2 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -16,6 +16,59 @@ class DeprecatedApi { final ApiClient apiClient; + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + Future createPartnerDeprecatedWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/partners/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + Future createPartnerDeprecated(String id,) async { + final response = await createPartnerDeprecatedWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PartnerResponseDto',) as PartnerResponseDto; + + } + return null; + } + /// This property was deprecated in v1.116.0. This endpoint requires the `asset.read` permission. /// /// Note: This method returns the HTTP [Response]. @@ -74,4 +127,138 @@ class DeprecatedApi { } return null; } + + /// Replace the asset with new file, without changing its id + /// + /// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [MultipartFile] assetData (required): + /// + /// * [String] deviceAssetId (required): + /// + /// * [String] deviceId (required): + /// + /// * [DateTime] fileCreatedAt (required): + /// + /// * [DateTime] fileModifiedAt (required): + /// + /// * [String] key: + /// + /// * [String] slug: + /// + /// * [String] duration: + /// + /// * [String] filename: + Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/original' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = ['multipart/form-data']; + + bool hasFields = false; + final mp = MultipartRequest('PUT', Uri.parse(apiPath)); + if (assetData != null) { + hasFields = true; + mp.fields[r'assetData'] = assetData.field; + mp.files.add(assetData); + } + if (deviceAssetId != null) { + hasFields = true; + mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); + } + if (deviceId != null) { + hasFields = true; + mp.fields[r'deviceId'] = parameterToString(deviceId); + } + if (duration != null) { + hasFields = true; + mp.fields[r'duration'] = parameterToString(duration); + } + if (fileCreatedAt != null) { + hasFields = true; + mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); + } + if (fileModifiedAt != null) { + hasFields = true; + mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); + } + if (filename != null) { + hasFields = true; + mp.fields[r'filename'] = parameterToString(filename); + } + if (hasFields) { + postBody = mp; + } + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Replace the asset with new file, without changing its id + /// + /// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [MultipartFile] assetData (required): + /// + /// * [String] deviceAssetId (required): + /// + /// * [String] deviceId (required): + /// + /// * [DateTime] fileCreatedAt (required): + /// + /// * [DateTime] fileModifiedAt (required): + /// + /// * [String] key: + /// + /// * [String] slug: + /// + /// * [String] duration: + /// + /// * [String] filename: + Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { + final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); + 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), 'AssetMediaResponseDto',) as AssetMediaResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index eb5d5f5806..a5fdf53ab5 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -22,8 +22,60 @@ class PartnersApi { /// /// Parameters: /// + /// * [PartnerCreateDto] partnerCreateDto (required): + Future createPartnerWithHttpInfo(PartnerCreateDto partnerCreateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/partners'; + + // ignore: prefer_final_locals + Object? postBody = partnerCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This endpoint requires the `partner.create` permission. + /// + /// Parameters: + /// + /// * [PartnerCreateDto] partnerCreateDto (required): + Future createPartner(PartnerCreateDto partnerCreateDto,) async { + final response = await createPartnerWithHttpInfo(partnerCreateDto,); + 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), 'PartnerResponseDto',) as PartnerResponseDto; + + } + return null; + } + + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// /// * [String] id (required): - Future createPartnerWithHttpInfo(String id,) async { + Future createPartnerDeprecatedWithHttpInfo(String id,) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -49,13 +101,13 @@ class PartnersApi { ); } - /// This endpoint requires the `partner.create` permission. + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. /// /// Parameters: /// /// * [String] id (required): - Future createPartner(String id,) async { - final response = await createPartnerWithHttpInfo(id,); + Future createPartnerDeprecated(String id,) async { + final response = await createPartnerDeprecatedWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -179,14 +231,14 @@ class PartnersApi { /// /// * [String] id (required): /// - /// * [UpdatePartnerDto] updatePartnerDto (required): - Future updatePartnerWithHttpInfo(String id, UpdatePartnerDto updatePartnerDto,) async { + /// * [PartnerUpdateDto] partnerUpdateDto (required): + Future updatePartnerWithHttpInfo(String id, PartnerUpdateDto partnerUpdateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = updatePartnerDto; + Object? postBody = partnerUpdateDto; final queryParams = []; final headerParams = {}; @@ -212,9 +264,9 @@ class PartnersApi { /// /// * [String] id (required): /// - /// * [UpdatePartnerDto] updatePartnerDto (required): - Future updatePartner(String id, UpdatePartnerDto updatePartnerDto,) async { - final response = await updatePartnerWithHttpInfo(id, updatePartnerDto,); + /// * [PartnerUpdateDto] partnerUpdateDto (required): + Future updatePartner(String id, PartnerUpdateDto partnerUpdateDto,) async { + final response = await updatePartnerWithHttpInfo(id, partnerUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 2d142e3d67..70ac076c9d 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -53,12 +53,15 @@ class TimelineApi { /// * [AssetVisibility] visibility: /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// + /// * [bool] withCoordinates: + /// Include location data in the response + /// /// * [bool] withPartners: /// Include assets shared by partners /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, 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? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/bucket'; @@ -100,6 +103,9 @@ class TimelineApi { if (visibility != null) { queryParams.addAll(_queryParams('', 'visibility', visibility)); } + if (withCoordinates != null) { + queryParams.addAll(_queryParams('', 'withCoordinates', withCoordinates)); + } if (withPartners != null) { queryParams.addAll(_queryParams('', 'withPartners', withPartners)); } @@ -156,13 +162,16 @@ class TimelineApi { /// * [AssetVisibility] visibility: /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// + /// * [bool] withCoordinates: + /// Include location data in the response + /// /// * [bool] withPartners: /// Include assets shared by partners /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, 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, slug: slug, 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? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -210,12 +219,15 @@ class TimelineApi { /// * [AssetVisibility] visibility: /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// + /// * [bool] withCoordinates: + /// Include location data in the response + /// /// * [bool] withPartners: /// Include assets shared by partners /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, 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? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/buckets'; @@ -256,6 +268,9 @@ class TimelineApi { if (visibility != null) { queryParams.addAll(_queryParams('', 'visibility', visibility)); } + if (withCoordinates != null) { + queryParams.addAll(_queryParams('', 'withCoordinates', withCoordinates)); + } if (withPartners != null) { queryParams.addAll(_queryParams('', 'withPartners', withPartners)); } @@ -309,13 +324,16 @@ class TimelineApi { /// * [AssetVisibility] visibility: /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// + /// * [bool] withCoordinates: + /// Include location data in the response + /// /// * [bool] withPartners: /// Include assets shared by partners /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, 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, slug: slug, 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? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 3f31d4ed90..06d27593c9 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -266,6 +266,14 @@ class ApiClient { return AssetMediaSizeTypeTransformer().decode(value); case 'AssetMediaStatus': return AssetMediaStatusTypeTransformer().decode(value); + case 'AssetMetadataKey': + return AssetMetadataKeyTypeTransformer().decode(value); + case 'AssetMetadataResponseDto': + return AssetMetadataResponseDto.fromJson(value); + case 'AssetMetadataUpsertDto': + return AssetMetadataUpsertDto.fromJson(value); + case 'AssetMetadataUpsertItemDto': + return AssetMetadataUpsertItemDto.fromJson(value); case 'AssetOrder': return AssetOrderTypeTransformer().decode(value); case 'AssetResponseDto': @@ -374,6 +382,8 @@ class ApiClient { return LoginResponseDto.fromJson(value); case 'LogoutResponseDto': return LogoutResponseDto.fromJson(value); + case 'MachineLearningAvailabilityChecksDto': + return MachineLearningAvailabilityChecksDto.fromJson(value); case 'ManualJobName': return ManualJobNameTypeTransformer().decode(value); case 'MapMarkerResponseDto': @@ -426,10 +436,14 @@ class ApiClient { return OnboardingDto.fromJson(value); case 'OnboardingResponseDto': return OnboardingResponseDto.fromJson(value); + case 'PartnerCreateDto': + return PartnerCreateDto.fromJson(value); case 'PartnerDirection': return PartnerDirectionTypeTransformer().decode(value); case 'PartnerResponseDto': return PartnerResponseDto.fromJson(value); + case 'PartnerUpdateDto': + return PartnerUpdateDto.fromJson(value); case 'PeopleResponse': return PeopleResponse.fromJson(value); case 'PeopleResponseDto': @@ -580,6 +594,10 @@ class ApiClient { return SyncAssetFaceDeleteV1.fromJson(value); case 'SyncAssetFaceV1': return SyncAssetFaceV1.fromJson(value); + case 'SyncAssetMetadataDeleteV1': + return SyncAssetMetadataDeleteV1.fromJson(value); + case 'SyncAssetMetadataV1': + return SyncAssetMetadataV1.fromJson(value); case 'SyncAssetV1': return SyncAssetV1.fromJson(value); case 'SyncAuthUserV1': @@ -722,8 +740,6 @@ class ApiClient { return UpdateAssetDto.fromJson(value); case 'UpdateLibraryDto': return UpdateLibraryDto.fromJson(value); - case 'UpdatePartnerDto': - return UpdatePartnerDto.fromJson(value); case 'UsageByUserDto': return UsageByUserDto.fromJson(value); case 'UserAdminCreateDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 4adb62768b..b34e9210c8 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -67,6 +67,9 @@ String parameterToString(dynamic value) { if (value is AssetMediaStatus) { return AssetMediaStatusTypeTransformer().encode(value).toString(); } + if (value is AssetMetadataKey) { + return AssetMetadataKeyTypeTransformer().encode(value).toString(); + } if (value is AssetOrder) { return AssetOrderTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/asset_metadata_key.dart b/mobile/openapi/lib/model/asset_metadata_key.dart new file mode 100644 index 0000000000..70186cd41c --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_key.dart @@ -0,0 +1,82 @@ +// +// 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 AssetMetadataKey { + /// Instantiate a new enum with the provided [value]. + const AssetMetadataKey._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const mobileApp = AssetMetadataKey._(r'mobile-app'); + + /// List of all possible values in this [enum][AssetMetadataKey]. + static const values = [ + mobileApp, + ]; + + static AssetMetadataKey? fromJson(dynamic value) => AssetMetadataKeyTypeTransformer().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 = AssetMetadataKey.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetMetadataKey] to String, +/// and [decode] dynamic data back to [AssetMetadataKey]. +class AssetMetadataKeyTypeTransformer { + factory AssetMetadataKeyTypeTransformer() => _instance ??= const AssetMetadataKeyTypeTransformer._(); + + const AssetMetadataKeyTypeTransformer._(); + + String encode(AssetMetadataKey data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetMetadataKey. + /// + /// 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. + AssetMetadataKey? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'mobile-app': return AssetMetadataKey.mobileApp; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetMetadataKeyTypeTransformer] instance. + static AssetMetadataKeyTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart new file mode 100644 index 0000000000..af5769b9bb --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_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 AssetMetadataResponseDto { + /// Returns a new [AssetMetadataResponseDto] instance. + AssetMetadataResponseDto({ + required this.key, + required this.updatedAt, + required this.value, + }); + + AssetMetadataKey key; + + DateTime updatedAt; + + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataResponseDto && + other.key == key && + other.updatedAt == updatedAt && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (key.hashCode) + + (updatedAt.hashCode) + + (value.hashCode); + + @override + String toString() => 'AssetMetadataResponseDto[key=$key, updatedAt=$updatedAt, value=$value]'; + + Map toJson() { + final json = {}; + json[r'key'] = this.key; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'value'] = this.value; + return json; + } + + /// Returns a new [AssetMetadataResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataResponseDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataResponseDto( + key: AssetMetadataKey.fromJson(json[r'key'])!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + value: mapValueOfType(json, r'value')!, + ); + } + 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 = AssetMetadataResponseDto.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 = AssetMetadataResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataResponseDto-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] = AssetMetadataResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'key', + 'updatedAt', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_dto.dart new file mode 100644 index 0000000000..45d044feb0 --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_upsert_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 AssetMetadataUpsertDto { + /// Returns a new [AssetMetadataUpsertDto] instance. + AssetMetadataUpsertDto({ + this.items = const [], + }); + + List items; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertDto && + _deepEquality.equals(other.items, items); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (items.hashCode); + + @override + String toString() => 'AssetMetadataUpsertDto[items=$items]'; + + Map toJson() { + final json = {}; + json[r'items'] = this.items; + return json; + } + + /// Returns a new [AssetMetadataUpsertDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataUpsertDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataUpsertDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataUpsertDto( + items: AssetMetadataUpsertItemDto.listFromJson(json[r'items']), + ); + } + 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 = AssetMetadataUpsertDto.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 = AssetMetadataUpsertDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataUpsertDto-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] = AssetMetadataUpsertDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'items', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart new file mode 100644 index 0000000000..4b7e6579a1 --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetMetadataUpsertItemDto { + /// Returns a new [AssetMetadataUpsertItemDto] instance. + AssetMetadataUpsertItemDto({ + required this.key, + required this.value, + }); + + AssetMetadataKey key; + + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertItemDto && + other.key == key && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (key.hashCode) + + (value.hashCode); + + @override + String toString() => 'AssetMetadataUpsertItemDto[key=$key, value=$value]'; + + Map toJson() { + final json = {}; + json[r'key'] = this.key; + json[r'value'] = this.value; + return json; + } + + /// Returns a new [AssetMetadataUpsertItemDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataUpsertItemDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataUpsertItemDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataUpsertItemDto( + key: AssetMetadataKey.fromJson(json[r'key'])!, + value: mapValueOfType(json, r'value')!, + ); + } + 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 = AssetMetadataUpsertItemDto.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 = AssetMetadataUpsertItemDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataUpsertItemDto-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] = AssetMetadataUpsertItemDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'key', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart b/mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart new file mode 100644 index 0000000000..84b3181426 --- /dev/null +++ b/mobile/openapi/lib/model/machine_learning_availability_checks_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 MachineLearningAvailabilityChecksDto { + /// Returns a new [MachineLearningAvailabilityChecksDto] instance. + MachineLearningAvailabilityChecksDto({ + required this.enabled, + required this.interval, + required this.timeout, + }); + + bool enabled; + + num interval; + + num timeout; + + @override + bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto && + other.enabled == enabled && + other.interval == interval && + other.timeout == timeout; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (interval.hashCode) + + (timeout.hashCode); + + @override + String toString() => 'MachineLearningAvailabilityChecksDto[enabled=$enabled, interval=$interval, timeout=$timeout]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'interval'] = this.interval; + json[r'timeout'] = this.timeout; + return json; + } + + /// Returns a new [MachineLearningAvailabilityChecksDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MachineLearningAvailabilityChecksDto? fromJson(dynamic value) { + upgradeDto(value, "MachineLearningAvailabilityChecksDto"); + if (value is Map) { + final json = value.cast(); + + return MachineLearningAvailabilityChecksDto( + enabled: mapValueOfType(json, r'enabled')!, + interval: num.parse('${json[r'interval']}'), + timeout: num.parse('${json[r'timeout']}'), + ); + } + 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 = MachineLearningAvailabilityChecksDto.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 = MachineLearningAvailabilityChecksDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MachineLearningAvailabilityChecksDto-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] = MachineLearningAvailabilityChecksDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'interval', + 'timeout', + }; +} + diff --git a/mobile/openapi/lib/model/partner_create_dto.dart b/mobile/openapi/lib/model/partner_create_dto.dart new file mode 100644 index 0000000000..09d60c5c77 --- /dev/null +++ b/mobile/openapi/lib/model/partner_create_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 PartnerCreateDto { + /// Returns a new [PartnerCreateDto] instance. + PartnerCreateDto({ + required this.sharedWithId, + }); + + String sharedWithId; + + @override + bool operator ==(Object other) => identical(this, other) || other is PartnerCreateDto && + other.sharedWithId == sharedWithId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (sharedWithId.hashCode); + + @override + String toString() => 'PartnerCreateDto[sharedWithId=$sharedWithId]'; + + Map toJson() { + final json = {}; + json[r'sharedWithId'] = this.sharedWithId; + return json; + } + + /// Returns a new [PartnerCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PartnerCreateDto? fromJson(dynamic value) { + upgradeDto(value, "PartnerCreateDto"); + if (value is Map) { + final json = value.cast(); + + return PartnerCreateDto( + sharedWithId: mapValueOfType(json, r'sharedWithId')!, + ); + } + 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 = PartnerCreateDto.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 = PartnerCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PartnerCreateDto-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] = PartnerCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'sharedWithId', + }; +} + diff --git a/mobile/openapi/lib/model/update_partner_dto.dart b/mobile/openapi/lib/model/partner_update_dto.dart similarity index 66% rename from mobile/openapi/lib/model/update_partner_dto.dart rename to mobile/openapi/lib/model/partner_update_dto.dart index 3af3c83ad1..25cf217764 100644 --- a/mobile/openapi/lib/model/update_partner_dto.dart +++ b/mobile/openapi/lib/model/partner_update_dto.dart @@ -10,16 +10,16 @@ part of openapi.api; -class UpdatePartnerDto { - /// Returns a new [UpdatePartnerDto] instance. - UpdatePartnerDto({ +class PartnerUpdateDto { + /// Returns a new [PartnerUpdateDto] instance. + PartnerUpdateDto({ required this.inTimeline, }); bool inTimeline; @override - bool operator ==(Object other) => identical(this, other) || other is UpdatePartnerDto && + bool operator ==(Object other) => identical(this, other) || other is PartnerUpdateDto && other.inTimeline == inTimeline; @override @@ -28,7 +28,7 @@ class UpdatePartnerDto { (inTimeline.hashCode); @override - String toString() => 'UpdatePartnerDto[inTimeline=$inTimeline]'; + String toString() => 'PartnerUpdateDto[inTimeline=$inTimeline]'; Map toJson() { final json = {}; @@ -36,26 +36,26 @@ class UpdatePartnerDto { return json; } - /// Returns a new [UpdatePartnerDto] instance and imports its values from + /// Returns a new [PartnerUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static UpdatePartnerDto? fromJson(dynamic value) { - upgradeDto(value, "UpdatePartnerDto"); + static PartnerUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "PartnerUpdateDto"); if (value is Map) { final json = value.cast(); - return UpdatePartnerDto( + return PartnerUpdateDto( inTimeline: mapValueOfType(json, r'inTimeline')!, ); } 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 = UpdatePartnerDto.fromJson(row); + final value = PartnerUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -64,12 +64,12 @@ class UpdatePartnerDto { 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 = UpdatePartnerDto.fromJson(entry.value); + final value = PartnerUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -78,14 +78,14 @@ class UpdatePartnerDto { return map; } - // maps a json object with a list of UpdatePartnerDto-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 PartnerUpdateDto-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] = UpdatePartnerDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = PartnerUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 0d16b56d74..90902b9791 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -31,7 +31,8 @@ class SmartSearchDto { this.model, this.page, this.personIds = const [], - required this.query, + this.query, + this.queryAssetId, this.rating, this.size, this.state, @@ -151,7 +152,21 @@ class SmartSearchDto { List personIds; - String query; + /// + /// 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? query; + + /// + /// 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? queryAssetId; /// Minimum value: -1 /// Maximum value: 5 @@ -278,6 +293,7 @@ class SmartSearchDto { other.page == page && _deepEquality.equals(other.personIds, personIds) && other.query == query && + other.queryAssetId == queryAssetId && other.rating == rating && other.size == size && other.state == state && @@ -314,7 +330,8 @@ class SmartSearchDto { (model == null ? 0 : model!.hashCode) + (page == null ? 0 : page!.hashCode) + (personIds.hashCode) + - (query.hashCode) + + (query == null ? 0 : query!.hashCode) + + (queryAssetId == null ? 0 : queryAssetId!.hashCode) + (rating == null ? 0 : rating!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + @@ -331,7 +348,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -417,7 +434,16 @@ class SmartSearchDto { // json[r'page'] = null; } json[r'personIds'] = this.personIds; + if (this.query != null) { json[r'query'] = this.query; + } else { + // json[r'query'] = null; + } + if (this.queryAssetId != null) { + json[r'queryAssetId'] = this.queryAssetId; + } else { + // json[r'queryAssetId'] = null; + } if (this.rating != null) { json[r'rating'] = this.rating; } else { @@ -522,7 +548,8 @@ class SmartSearchDto { personIds: json[r'personIds'] is Iterable ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], - query: mapValueOfType(json, r'query')!, + query: mapValueOfType(json, r'query'), + queryAssetId: mapValueOfType(json, r'queryAssetId'), rating: num.parse('${json[r'rating']}'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), @@ -586,7 +613,6 @@ class SmartSearchDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'query', }; } diff --git a/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart new file mode 100644 index 0000000000..c9a7ef4670 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_metadata_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 SyncAssetMetadataDeleteV1 { + /// Returns a new [SyncAssetMetadataDeleteV1] instance. + SyncAssetMetadataDeleteV1({ + required this.assetId, + required this.key, + }); + + String assetId; + + AssetMetadataKey key; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataDeleteV1 && + other.assetId == assetId && + other.key == key; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (key.hashCode); + + @override + String toString() => 'SyncAssetMetadataDeleteV1[assetId=$assetId, key=$key]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'key'] = this.key; + return json; + } + + /// Returns a new [SyncAssetMetadataDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetMetadataDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetMetadataDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetMetadataDeleteV1( + assetId: mapValueOfType(json, r'assetId')!, + key: AssetMetadataKey.fromJson(json[r'key'])!, + ); + } + 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 = SyncAssetMetadataDeleteV1.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 = SyncAssetMetadataDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetMetadataDeleteV1-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] = SyncAssetMetadataDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'key', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart new file mode 100644 index 0000000000..720fcef947 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_metadata_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 SyncAssetMetadataV1 { + /// Returns a new [SyncAssetMetadataV1] instance. + SyncAssetMetadataV1({ + required this.assetId, + required this.key, + required this.value, + }); + + String assetId; + + AssetMetadataKey key; + + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataV1 && + other.assetId == assetId && + other.key == key && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (key.hashCode) + + (value.hashCode); + + @override + String toString() => 'SyncAssetMetadataV1[assetId=$assetId, key=$key, value=$value]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'key'] = this.key; + json[r'value'] = this.value; + return json; + } + + /// Returns a new [SyncAssetMetadataV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetMetadataV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetMetadataV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetMetadataV1( + assetId: mapValueOfType(json, r'assetId')!, + key: AssetMetadataKey.fromJson(json[r'key'])!, + value: mapValueOfType(json, r'value')!, + ); + } + 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 = SyncAssetMetadataV1.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 = SyncAssetMetadataV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetMetadataV1-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] = SyncAssetMetadataV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'key', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index f259fdc9d9..1b4ca91f3b 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -29,6 +29,8 @@ class SyncEntityType { static const assetV1 = SyncEntityType._(r'AssetV1'); static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); + static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1'); + static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1'); static const partnerV1 = SyncEntityType._(r'PartnerV1'); static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); @@ -67,6 +69,7 @@ class SyncEntityType { static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); static const syncResetV1 = SyncEntityType._(r'SyncResetV1'); + static const syncCompleteV1 = SyncEntityType._(r'SyncCompleteV1'); /// List of all possible values in this [enum][SyncEntityType]. static const values = [ @@ -76,6 +79,8 @@ class SyncEntityType { assetV1, assetDeleteV1, assetExifV1, + assetMetadataV1, + assetMetadataDeleteV1, partnerV1, partnerDeleteV1, partnerAssetV1, @@ -114,6 +119,7 @@ class SyncEntityType { userMetadataDeleteV1, syncAckV1, syncResetV1, + syncCompleteV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -158,6 +164,8 @@ class SyncEntityTypeTypeTransformer { case r'AssetV1': return SyncEntityType.assetV1; case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; case r'AssetExifV1': return SyncEntityType.assetExifV1; + case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1; + case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1; case r'PartnerV1': return SyncEntityType.partnerV1; case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; @@ -196,6 +204,7 @@ class SyncEntityTypeTypeTransformer { case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; case r'SyncAckV1': return SyncEntityType.syncAckV1; case r'SyncResetV1': return SyncEntityType.syncResetV1; + case r'SyncCompleteV1': return SyncEntityType.syncCompleteV1; 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 8a1857366e..c3dc1c4d61 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -30,6 +30,7 @@ class SyncRequestType { static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); static const assetsV1 = SyncRequestType._(r'AssetsV1'); static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); + static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); @@ -52,6 +53,7 @@ class SyncRequestType { albumAssetExifsV1, assetsV1, assetExifsV1, + assetMetadataV1, authUsersV1, memoriesV1, memoryToAssetsV1, @@ -109,6 +111,7 @@ class SyncRequestTypeTypeTransformer { case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; case r'AssetsV1': return SyncRequestType.assetsV1; case r'AssetExifsV1': return SyncRequestType.assetExifsV1; + case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; case r'AuthUsersV1': return SyncRequestType.authUsersV1; case r'MemoriesV1': return SyncRequestType.memoriesV1; case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index a4a9ca7d82..d7b2566d59 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -13,14 +13,16 @@ part of openapi.api; class SystemConfigMachineLearningDto { /// Returns a new [SystemConfigMachineLearningDto] instance. SystemConfigMachineLearningDto({ + required this.availabilityChecks, required this.clip, required this.duplicateDetection, required this.enabled, required this.facialRecognition, - this.url, this.urls = const [], }); + MachineLearningAvailabilityChecksDto availabilityChecks; + CLIPConfig clip; DuplicateDetectionConfig duplicateDetection; @@ -29,50 +31,37 @@ class SystemConfigMachineLearningDto { FacialRecognitionConfig facialRecognition; - /// This property was deprecated in v1.122.0 - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? url; - List urls; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigMachineLearningDto && + other.availabilityChecks == availabilityChecks && other.clip == clip && other.duplicateDetection == duplicateDetection && other.enabled == enabled && other.facialRecognition == facialRecognition && - other.url == url && _deepEquality.equals(other.urls, urls); @override int get hashCode => // ignore: unnecessary_parenthesis + (availabilityChecks.hashCode) + (clip.hashCode) + (duplicateDetection.hashCode) + (enabled.hashCode) + (facialRecognition.hashCode) + - (url == null ? 0 : url!.hashCode) + (urls.hashCode); @override - String toString() => 'SystemConfigMachineLearningDto[clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, url=$url, urls=$urls]'; + String toString() => 'SystemConfigMachineLearningDto[availabilityChecks=$availabilityChecks, clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, urls=$urls]'; Map toJson() { final json = {}; + json[r'availabilityChecks'] = this.availabilityChecks; json[r'clip'] = this.clip; json[r'duplicateDetection'] = this.duplicateDetection; json[r'enabled'] = this.enabled; json[r'facialRecognition'] = this.facialRecognition; - if (this.url != null) { - json[r'url'] = this.url; - } else { - // json[r'url'] = null; - } json[r'urls'] = this.urls; return json; } @@ -86,11 +75,11 @@ class SystemConfigMachineLearningDto { final json = value.cast(); return SystemConfigMachineLearningDto( + availabilityChecks: MachineLearningAvailabilityChecksDto.fromJson(json[r'availabilityChecks'])!, clip: CLIPConfig.fromJson(json[r'clip'])!, duplicateDetection: DuplicateDetectionConfig.fromJson(json[r'duplicateDetection'])!, enabled: mapValueOfType(json, r'enabled')!, facialRecognition: FacialRecognitionConfig.fromJson(json[r'facialRecognition'])!, - url: mapValueOfType(json, r'url'), urls: json[r'urls'] is Iterable ? (json[r'urls'] as Iterable).cast().toList(growable: false) : const [], @@ -141,6 +130,7 @@ class SystemConfigMachineLearningDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'availabilityChecks', 'clip', 'duplicateDetection', 'enabled', diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart index 886b353f68..58032b7c51 100644 --- a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -21,8 +21,10 @@ class TimeBucketAssetResponseDto { this.isFavorite = const [], this.isImage = const [], this.isTrashed = const [], + this.latitude = const [], this.livePhotoVideoId = const [], this.localOffsetHours = const [], + this.longitude = const [], this.ownerId = const [], this.projectionType = const [], this.ratio = const [], @@ -55,12 +57,18 @@ class TimeBucketAssetResponseDto { /// Array indicating whether each asset is in the trash List isTrashed; + /// Array of latitude coordinates extracted from EXIF GPS data + List latitude; + /// Array of live photo video asset IDs (null for non-live photos) List livePhotoVideoId; /// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. List localOffsetHours; + /// Array of longitude coordinates extracted from EXIF GPS data + List longitude; + /// Array of owner IDs for each asset List ownerId; @@ -89,8 +97,10 @@ class TimeBucketAssetResponseDto { _deepEquality.equals(other.isFavorite, isFavorite) && _deepEquality.equals(other.isImage, isImage) && _deepEquality.equals(other.isTrashed, isTrashed) && + _deepEquality.equals(other.latitude, latitude) && _deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) && _deepEquality.equals(other.localOffsetHours, localOffsetHours) && + _deepEquality.equals(other.longitude, longitude) && _deepEquality.equals(other.ownerId, ownerId) && _deepEquality.equals(other.projectionType, projectionType) && _deepEquality.equals(other.ratio, ratio) && @@ -109,8 +119,10 @@ class TimeBucketAssetResponseDto { (isFavorite.hashCode) + (isImage.hashCode) + (isTrashed.hashCode) + + (latitude.hashCode) + (livePhotoVideoId.hashCode) + (localOffsetHours.hashCode) + + (longitude.hashCode) + (ownerId.hashCode) + (projectionType.hashCode) + (ratio.hashCode) + @@ -119,7 +131,7 @@ class TimeBucketAssetResponseDto { (visibility.hashCode); @override - String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, fileCreatedAt=$fileCreatedAt, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, livePhotoVideoId=$livePhotoVideoId, localOffsetHours=$localOffsetHours, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]'; + String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, fileCreatedAt=$fileCreatedAt, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, localOffsetHours=$localOffsetHours, longitude=$longitude, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]'; Map toJson() { final json = {}; @@ -131,8 +143,10 @@ class TimeBucketAssetResponseDto { json[r'isFavorite'] = this.isFavorite; json[r'isImage'] = this.isImage; json[r'isTrashed'] = this.isTrashed; + json[r'latitude'] = this.latitude; json[r'livePhotoVideoId'] = this.livePhotoVideoId; json[r'localOffsetHours'] = this.localOffsetHours; + json[r'longitude'] = this.longitude; json[r'ownerId'] = this.ownerId; json[r'projectionType'] = this.projectionType; json[r'ratio'] = this.ratio; @@ -175,12 +189,18 @@ class TimeBucketAssetResponseDto { isTrashed: json[r'isTrashed'] is Iterable ? (json[r'isTrashed'] as Iterable).cast().toList(growable: false) : const [], + latitude: json[r'latitude'] is Iterable + ? (json[r'latitude'] as Iterable).cast().toList(growable: false) + : const [], livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable ? (json[r'livePhotoVideoId'] as Iterable).cast().toList(growable: false) : const [], localOffsetHours: json[r'localOffsetHours'] is Iterable ? (json[r'localOffsetHours'] as Iterable).cast().toList(growable: false) : const [], + longitude: json[r'longitude'] is Iterable + ? (json[r'longitude'] as Iterable).cast().toList(growable: false) + : const [], ownerId: json[r'ownerId'] is Iterable ? (json[r'ownerId'] as Iterable).cast().toList(growable: false) : const [], diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart new file mode 100644 index 0000000000..6f6c781de2 --- /dev/null +++ b/mobile/pigeon/background_worker_api.dart @@ -0,0 +1,54 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/background_worker_api.g.dart', + swiftOut: 'ios/Runner/Background/BackgroundWorker.g.swift', + swiftOptions: SwiftOptions(includeErrorClass: false), + kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.background'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +class BackgroundWorkerSettings { + final bool requiresCharging; + final int minimumDelaySeconds; + + const BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds}); +} + +@HostApi() +abstract class BackgroundWorkerFgHostApi { + void enable(); + + void configure(BackgroundWorkerSettings settings); + + void disable(); +} + +@HostApi() +abstract class BackgroundWorkerBgHostApi { + // Called from the background flutter engine when it has bootstrapped and established the + // required platform channels to notify the native side to start the background upload + void onInitialized(); + + void showNotification(String title, String content); + + // Called from the background flutter engine to request the native side to cleanup + void close(); +} + +@FlutterApi() +abstract class BackgroundWorkerFlutterApi { + // iOS Only: Called when the iOS background upload is triggered + @async + void onIosUpload(bool isRefresh, int? maxSeconds); + + // Android Only: Called when the Android background upload is triggered + @async + void onAndroidUpload(); + + @async + void cancel(); +} diff --git a/mobile/pigeon/background_worker_lock_api.dart b/mobile/pigeon/background_worker_lock_api.dart new file mode 100644 index 0000000000..44c5220367 --- /dev/null +++ b/mobile/pigeon/background_worker_lock_api.dart @@ -0,0 +1,17 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/background_worker_lock_api.g.dart', + kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.background', includeErrorClass: false), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +@HostApi() +abstract class BackgroundWorkerLockApi { + void lock(); + + void unlock(); +} diff --git a/mobile/pigeon/connectivity_api.dart b/mobile/pigeon/connectivity_api.dart new file mode 100644 index 0000000000..c5677ee20e --- /dev/null +++ b/mobile/pigeon/connectivity_api.dart @@ -0,0 +1,20 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/connectivity_api.g.dart', + swiftOut: 'ios/Runner/Connectivity/Connectivity.g.swift', + swiftOptions: SwiftOptions(includeErrorClass: false), + kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.connectivity'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +enum NetworkCapability { cellular, wifi, vpn, unmetered } + +@HostApi() +abstract class ConnectivityApi { + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getCapabilities(); +} diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart index e84c814c3d..ac08a68ca3 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -71,6 +71,14 @@ class SyncDelta { }); } +class HashResult { + final String assetId; + final String? error; + final String? hash; + + const HashResult({required this.assetId, this.error, this.hash}); +} + @HostApi() abstract class NativeSyncApi { bool shouldFullSync(); @@ -94,6 +102,9 @@ abstract class NativeSyncApi { @TaskQueue(type: TaskQueueType.serialBackgroundThread) List getAssetsForAlbum(String albumId, {int? updatedTimeCond}); + @async @TaskQueue(type: TaskQueueType.serialBackgroundThread) - List hashPaths(List paths); + List hashAssets(List assetIds, {bool allowNetworkAccess = false}); + + void cancelHashing(); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 624ed1fe65..e5f972743c 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: "direct main" description: name: background_downloader - sha256: "2d4c2b7438e7643585880f9cc00ace16a52d778088751f1bfbf714627b315462" + sha256: "9ed74c55750932178f6989ba8a659687c2a102e05b70f561a1b3f047a5dda790" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "9.2.5" bonsoir: dependency: transitive description: @@ -1064,26 +1064,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -1208,8 +1208,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5459d54" - resolved-ref: "5459d54cdc1cf4d99e2193b310052f1ebb5dcf43" + ref: "893894b" + resolved-ref: "893894b98b832be8a995a8d5d4c2289d0ad2d246" url: "https://github.com/immich-app/native_video_player" source: git version: "1.3.1" @@ -1429,7 +1429,7 @@ packages: source: hosted version: "5.0.1" platform: - dependency: "direct main" + dependency: transitive description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" @@ -1877,10 +1877,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" thumbhash: dependency: "direct main" description: @@ -2037,10 +2037,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -2171,4 +2171,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.8" + flutter: ">=3.35.4" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 7d40c80a26..6e143c448d 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,11 +2,11 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.139.4+3009 +version: 2.0.0+3020 environment: sdk: '>=3.8.0 <4.0.0' - flutter: 3.32.8 + flutter: 3.35.4 isar_version: &isar_version 3.1.8 @@ -16,7 +16,7 @@ dependencies: async: ^2.11.0 auto_route: ^9.2.0 - background_downloader: ^9.2.0 + background_downloader: ^9.2.5 cached_network_image: ^3.4.1 cancellation_token_http: ^2.1.0 cast: ^2.1.0 @@ -57,7 +57,6 @@ dependencies: 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 @@ -78,7 +77,7 @@ dependencies: native_video_player: git: url: https://github.com/immich-app/native_video_player - ref: '5459d54' + ref: '893894b' openapi: path: openapi isar: diff --git a/mobile/test/domain/services/hash_service_test.dart b/mobile/test/domain/services/hash_service_test.dart index 7969131e7f..20d60b6866 100644 --- a/mobile/test/domain/services/hash_service_test.dart +++ b/mobile/test/domain/services/hash_service_test.dart @@ -1,11 +1,7 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/hash.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:mocktail/mocktail.dart'; import '../../fixtures/album.stub.dart'; @@ -13,192 +9,137 @@ import '../../fixtures/asset.stub.dart'; import '../../infrastructure/repository.mock.dart'; import '../service.mock.dart'; -class MockFile extends Mock implements File {} - void main() { late HashService sut; late MockLocalAlbumRepository mockAlbumRepo; late MockLocalAssetRepository mockAssetRepo; - late MockStorageRepository mockStorageRepo; late MockNativeSyncApi mockNativeApi; - final sortBy = {SortLocalAlbumsBy.backupSelection, SortLocalAlbumsBy.isIosSharedAlbum}; setUp(() { mockAlbumRepo = MockLocalAlbumRepository(); mockAssetRepo = MockLocalAssetRepository(); - mockStorageRepo = MockStorageRepository(); mockNativeApi = MockNativeSyncApi(); sut = HashService( localAlbumRepository: mockAlbumRepo, localAssetRepository: mockAssetRepo, - storageRepository: mockStorageRepo, nativeSyncApi: mockNativeApi, ); registerFallbackValue(LocalAlbumStub.recent); registerFallbackValue(LocalAssetStub.image1); + registerFallbackValue({}); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); - when(() => mockStorageRepo.clearCache()).thenAnswer((_) async => {}); }); group('HashService hashAssets', () { test('skips albums with no assets to hash', () async { when( - () => mockAlbumRepo.getAll(sortBy: sortBy), + () => mockAlbumRepo.getBackupAlbums(), ).thenAnswer((_) async => [LocalAlbumStub.recent.copyWith(assetCount: 0)]); when(() => mockAlbumRepo.getAssetsToHash(LocalAlbumStub.recent.id)).thenAnswer((_) async => []); await sut.hashAssets(); - verifyNever(() => mockStorageRepo.getFileForAsset(any())); - verifyNever(() => mockNativeApi.hashPaths(any())); + verifyNever(() => mockNativeApi.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'))); }); }); group('HashService _hashAssets', () { - test('skips assets without files', () async { + test('skips empty batches', () async { final album = LocalAlbumStub.recent; - final asset = LocalAssetStub.image1; - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => null); + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => []); await sut.hashAssets(); - verifyNever(() => mockNativeApi.hashPaths(any())); + verifyNever(() => mockNativeApi.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'))); }); test('processes assets when available', () async { final album = LocalAlbumStub.recent; final asset = LocalAssetStub.image1; - final mockFile = MockFile(); - final hash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockFile.length()).thenAnswer((_) async => 1000); - when(() => mockFile.path).thenReturn('image-path'); - - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer((_) async => [hash]); + when( + () => mockNativeApi.hashAssets([asset.id], allowNetworkAccess: false), + ).thenAnswer((_) async => [HashResult(assetId: asset.id, hash: 'test-hash')]); await sut.hashAssets(); - verify(() => mockNativeApi.hashPaths(['image-path'])).called(1); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; + verify(() => mockNativeApi.hashAssets([asset.id], allowNetworkAccess: false)).called(1); + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as Map; expect(captured.length, 1); - expect(captured[0].checksum, base64.encode(hash)); + expect(captured[asset.id], 'test-hash'); }); test('handles failed hashes', () async { final album = LocalAlbumStub.recent; final asset = LocalAssetStub.image1; - final mockFile = MockFile(); - when(() => mockFile.length()).thenAnswer((_) async => 1000); - when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer((_) async => [null]); - when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); + when( + () => mockNativeApi.hashAssets([asset.id], allowNetworkAccess: false), + ).thenAnswer((_) async => [HashResult(assetId: asset.id, error: 'Failed to hash')]); await sut.hashAssets(); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as Map; expect(captured.length, 0); }); - test('handles invalid hash length', () async { + test('handles null hash results', () async { final album = LocalAlbumStub.recent; final asset = LocalAssetStub.image1; - final mockFile = MockFile(); - when(() => mockFile.length()).thenAnswer((_) async => 1000); - when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - - final invalidHash = Uint8List.fromList([1, 2, 3]); - when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer((_) async => [invalidHash]); - when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); + when( + () => mockNativeApi.hashAssets([asset.id], allowNetworkAccess: false), + ).thenAnswer((_) async => [HashResult(assetId: asset.id, hash: null)]); await sut.hashAssets(); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as Map; expect(captured.length, 0); }); - test('batches by file count limit', () async { - final sut = HashService( - localAlbumRepository: mockAlbumRepo, - localAssetRepository: mockAssetRepo, - storageRepository: mockStorageRepo, - nativeSyncApi: mockNativeApi, - batchFileLimit: 1, - ); - - final album = LocalAlbumStub.recent; - final asset1 = LocalAssetStub.image1; - final asset2 = LocalAssetStub.image2; - final mockFile1 = MockFile(); - final mockFile2 = MockFile(); - when(() => mockFile1.length()).thenAnswer((_) async => 100); - when(() => mockFile1.path).thenReturn('path-1'); - when(() => mockFile2.length()).thenAnswer((_) async => 100); - when(() => mockFile2.path).thenReturn('path-2'); - - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1.id)).thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2.id)).thenAnswer((_) async => mockFile2); - - final hash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockNativeApi.hashPaths(any())).thenAnswer((_) async => [hash]); - when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); - - await sut.hashAssets(); - - verify(() => mockNativeApi.hashPaths(['path-1'])).called(1); - verify(() => mockNativeApi.hashPaths(['path-2'])).called(1); - verify(() => mockAssetRepo.updateHashes(any())).called(2); - }); - test('batches by size limit', () async { + const batchSize = 2; final sut = HashService( localAlbumRepository: mockAlbumRepo, localAssetRepository: mockAssetRepo, - storageRepository: mockStorageRepo, nativeSyncApi: mockNativeApi, - batchSizeLimit: 80, + batchSize: batchSize, ); final album = LocalAlbumStub.recent; final asset1 = LocalAssetStub.image1; final asset2 = LocalAssetStub.image2; - final mockFile1 = MockFile(); - final mockFile2 = MockFile(); - when(() => mockFile1.length()).thenAnswer((_) async => 100); - when(() => mockFile1.path).thenReturn('path-1'); - when(() => mockFile2.length()).thenAnswer((_) async => 100); - when(() => mockFile2.path).thenReturn('path-2'); + final asset3 = LocalAssetStub.image1.copyWith(id: 'image3', name: 'image3.jpg'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); - when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1.id)).thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2.id)).thenAnswer((_) async => mockFile2); + final capturedCalls = >[]; - final hash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockNativeApi.hashPaths(any())).thenAnswer((_) async => [hash]); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2, asset3]); + when(() => mockNativeApi.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'))).thenAnswer(( + invocation, + ) async { + final assetIds = invocation.positionalArguments[0] as List; + capturedCalls.add(List.from(assetIds)); + return assetIds.map((id) => HashResult(assetId: id, hash: '$id-hash')).toList(); + }); await sut.hashAssets(); - verify(() => mockNativeApi.hashPaths(['path-1'])).called(1); - verify(() => mockNativeApi.hashPaths(['path-2'])).called(1); + expect(capturedCalls.length, 2, reason: 'Should make exactly 2 calls to hashAssets'); + expect(capturedCalls[0], [asset1.id, asset2.id], reason: 'First call should batch the first two assets'); + expect(capturedCalls[1], [asset3.id], reason: 'Second call should have the remaining asset'); + verify(() => mockAssetRepo.updateHashes(any())).called(2); }); @@ -206,27 +147,43 @@ void main() { final album = LocalAlbumStub.recent; final asset1 = LocalAssetStub.image1; final asset2 = LocalAssetStub.image2; - final mockFile1 = MockFile(); - final mockFile2 = MockFile(); - when(() => mockFile1.length()).thenAnswer((_) async => 100); - when(() => mockFile1.path).thenReturn('path-1'); - when(() => mockFile2.length()).thenAnswer((_) async => 100); - when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1.id)).thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2.id)).thenAnswer((_) async => mockFile2); - - final validHash = Uint8List.fromList(List.generate(20, (i) => i)); - when(() => mockNativeApi.hashPaths(['path-1', 'path-2'])).thenAnswer((_) async => [validHash, null]); - when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); + when(() => mockNativeApi.hashAssets([asset1.id, asset2.id], allowNetworkAccess: false)).thenAnswer( + (_) async => [ + HashResult(assetId: asset1.id, hash: 'asset1-hash'), + HashResult(assetId: asset2.id, error: 'Failed to hash asset2'), + ], + ); await sut.hashAssets(); - final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List; + final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as Map; expect(captured.length, 1); - expect(captured.first.id, asset1.id); + expect(captured[asset1.id], 'asset1-hash'); + }); + + test('uses allowNetworkAccess based on album backup selection', () async { + final selectedAlbum = LocalAlbumStub.recent.copyWith(backupSelection: BackupSelection.selected); + final nonSelectedAlbum = LocalAlbumStub.recent.copyWith(id: 'album2', backupSelection: BackupSelection.excluded); + final asset1 = LocalAssetStub.image1; + final asset2 = LocalAssetStub.image2; + + when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [selectedAlbum, nonSelectedAlbum]); + when(() => mockAlbumRepo.getAssetsToHash(selectedAlbum.id)).thenAnswer((_) async => [asset1]); + when(() => mockAlbumRepo.getAssetsToHash(nonSelectedAlbum.id)).thenAnswer((_) async => [asset2]); + when(() => mockNativeApi.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'))).thenAnswer(( + invocation, + ) async { + final assetIds = invocation.positionalArguments[0] as List; + return assetIds.map((id) => HashResult(assetId: id, hash: '$id-hash')).toList(); + }); + + await sut.hashAssets(); + + verify(() => mockNativeApi.hashAssets([asset1.id], allowNetworkAccess: true)).called(1); + verify(() => mockNativeApi.hashAssets([asset2.id], allowNetworkAccess: false)).called(1); }); }); } diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 46e585faa0..0126b11e46 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -30,8 +30,9 @@ void main() { late SyncStreamService sut; late SyncStreamRepository mockSyncStreamRepo; late SyncApiRepository mockSyncApiRepo; - late Function(List, Function()) handleEventsCallback; + late Future Function(List, Function(), Function()) handleEventsCallback; late _MockAbortCallbackWrapper mockAbortCallbackWrapper; + late _MockAbortCallbackWrapper mockResetCallbackWrapper; successHandler(Invocation _) async => true; @@ -39,6 +40,7 @@ void main() { mockSyncStreamRepo = MockSyncStreamRepository(); mockSyncApiRepo = MockSyncApiRepository(); mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); + mockResetCallbackWrapper = _MockAbortCallbackWrapper(); when(() => mockAbortCallbackWrapper()).thenReturn(false); @@ -46,6 +48,10 @@ void main() { handleEventsCallback = invocation.positionalArguments.first; }); + when(() => mockSyncApiRepo.streamChanges(any(), onReset: any(named: 'onReset'))).thenAnswer((invocation) async { + handleEventsCallback = invocation.positionalArguments.first; + }); + when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {}); when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler); @@ -86,7 +92,7 @@ void main() { Future simulateEvents(List events) async { await sut.sync(); - await handleEventsCallback(events, mockAbortCallbackWrapper.call); + await handleEventsCallback(events, mockAbortCallbackWrapper.call, mockResetCallbackWrapper.call); } group("SyncStreamService - _handleEvents", () { @@ -156,7 +162,7 @@ void main() { when(() => cancellationChecker()).thenReturn(true); }); - await handleEventsCallback(events, mockAbortCallbackWrapper.call); + await handleEventsCallback(events, mockAbortCallbackWrapper.call, mockResetCallbackWrapper.call); verify(() => mockSyncStreamRepo.deleteUsersV1(any())).called(1); verifyNever(() => mockSyncStreamRepo.updateUsersV1(any())); @@ -188,7 +194,11 @@ void main() { final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin, SyncStreamStub.partnerDeleteV1]; - final processingFuture = handleEventsCallback(events, mockAbortCallbackWrapper.call); + final processingFuture = handleEventsCallback( + events, + mockAbortCallbackWrapper.call, + mockResetCallbackWrapper.call, + ); await pumpEventQueue(); expect(handler1Started, isTrue); diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 746206e453..073a86078f 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -11,6 +11,10 @@ import 'schema_v5.dart' as v5; import 'schema_v6.dart' as v6; import 'schema_v7.dart' as v7; import 'schema_v8.dart' as v8; +import 'schema_v9.dart' as v9; +import 'schema_v10.dart' as v10; +import 'schema_v11.dart' as v11; +import 'schema_v12.dart' as v12; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -32,10 +36,18 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v7.DatabaseAtV7(db); case 8: return v8.DatabaseAtV8(db); + case 9: + return v9.DatabaseAtV9(db); + case 10: + return v10.DatabaseAtV10(db); + case 11: + return v11.DatabaseAtV11(db); + case 12: + return v12.DatabaseAtV12(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; } diff --git a/mobile/test/drift/main/generated/schema_v10.dart b/mobile/test/drift/main/generated/schema_v10.dart new file mode 100644 index 0000000000..ba75530242 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v10.dart @@ -0,0 +1,7159 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + 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 String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + 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']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + ); + RemoteAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + 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 (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(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, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + 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('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: 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 + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements 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'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + 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 LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: 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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements 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 double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + 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.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= 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']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= 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), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => 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, + lens: lens.present ? lens.value : this.lens, + 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(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, + lens: data.lens.present ? data.lens.value : this.lens, + 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('lens: $lens, ') + ..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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is 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.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return 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 (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return 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, + lens: lens ?? this.lens, + 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'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = 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('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV10 extends GeneratedDatabase { + DatabaseAtV10(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + @override + int get schemaVersion => 10; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v11.dart b/mobile/test/drift/main/generated/schema_v11.dart new file mode 100644 index 0000000000..fe27f1bb3d --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v11.dart @@ -0,0 +1,7198 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + 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 String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + 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']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + ); + RemoteAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + 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 (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(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, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + 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('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: 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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements 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 double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + 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.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= 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']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= 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), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => 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, + lens: lens.present ? lens.value : this.lens, + 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(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, + lens: data.lens.present ? data.lens.value : this.lens, + 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('lens: $lens, ') + ..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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is 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.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return 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 (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return 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, + lens: lens ?? this.lens, + 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'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = 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('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV11 extends GeneratedDatabase { + DatabaseAtV11(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + @override + int get schemaVersion => 11; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v12.dart b/mobile/test/drift/main/generated/schema_v12.dart new file mode 100644 index 0000000000..c42df284ec --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v12.dart @@ -0,0 +1,7198 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + 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 String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + 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']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + ); + RemoteAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + 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 (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(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, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + 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('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: 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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements 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 double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + 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.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= 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']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= 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), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => 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, + lens: lens.present ? lens.value : this.lens, + 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(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, + lens: data.lens.present ? data.lens.value : this.lens, + 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('lens: $lens, ') + ..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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is 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.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return 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 (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return 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, + lens: lens ?? this.lens, + 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'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = 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('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV12 extends GeneratedDatabase { + DatabaseAtV12(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + @override + int get schemaVersion => 12; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v9.dart b/mobile/test/drift/main/generated/schema_v9.dart new file mode 100644 index 0000000000..c2ccb58737 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v9.dart @@ -0,0 +1,6712 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [ + id, + name, + isAdmin, + email, + hasProfileImage, + profileChangedAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final DateTime updatedAt; + const UserEntityData({ + required this.id, + required this.name, + required this.isAdmin, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + bool? isAdmin, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + DateTime? updatedAt, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + isAdmin, + email, + hasProfileImage, + profileChangedAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.updatedAt == this.updatedAt); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value updatedAt; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.updatedAt = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.updatedAt = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? updatedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (updatedAt != null) 'updated_at': updatedAt, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? updatedAt, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + 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 String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + 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']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + ); + RemoteAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + 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 (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..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('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + 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, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(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, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + 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('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: 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 + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements 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'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + 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 LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: 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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements 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 double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + 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.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= 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']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= 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), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => 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, + lens: lens.present ? lens.value : this.lens, + 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(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, + lens: data.lens.present ? data.lens.value : this.lens, + 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('lens: $lens, ') + ..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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is 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.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return 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 (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return 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, + lens: lens ?? this.lens, + 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'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = 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('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV9 extends GeneratedDatabase { + DatabaseAtV9(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + idxLatLng, + ]; + @override + int get schemaVersion => 9; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 369e62440d..2ba7177f89 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; abstract final class UserStub { const UserStub._(); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index d456b06f7c..467e19bf3f 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -4,12 +4,15 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../../api.mocks.dart'; import '../../service.mocks.dart'; +import '../../test_utils.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -33,6 +36,10 @@ void main() { late StreamController> responseStreamController; late int testBatchSize = 3; + setUpAll(() async { + await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar())); + }); + setUp(() { mockApiService = MockApiService(); mockApiClient = MockApiClient(); @@ -63,7 +70,9 @@ void main() { } }); - Future streamChanges(Function(List, Function() abort) onDataCallback) { + Future streamChanges( + Future Function(List, Function() abort, Function() reset) onDataCallback, + ) { return sut.streamChanges(onDataCallback, batchSize: testBatchSize, httpClient: mockHttpClient); } @@ -72,7 +81,7 @@ void main() { bool abortWasCalledInCallback = false; List receivedEventsBatch1 = []; - onDataCallback(List events, Function() abort) { + Future onDataCallback(List events, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { receivedEventsBatch1 = events; @@ -116,7 +125,7 @@ void main() { int onDataCallCount = 0; bool abortWasCalledInCallback = false; - onDataCallback(List events, Function() abort) { + Future onDataCallback(List events, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { abort(); @@ -158,7 +167,7 @@ void main() { List receivedEventsBatch1 = []; List receivedEventsBatch2 = []; - onDataCallback(List events, Function() _) { + Future onDataCallback(List events, Function() _, Function() __) async { onDataCallCount++; if (onDataCallCount == 1) { receivedEventsBatch1 = events; @@ -202,7 +211,7 @@ void main() { final streamError = Exception("Network Error"); int onDataCallCount = 0; - onDataCallback(List events, Function() _) { + Future onDataCallback(List events, Function() _, Function() __) async { onDataCallCount++; } @@ -229,8 +238,7 @@ void main() { when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; - - onDataCallback(List events, Function() _) { + Future onDataCallback(List events, Function() _, Function() __) async { onDataCallCount++; } diff --git a/mobile/test/modules/utils/datetime_helpers_test.dart b/mobile/test/modules/utils/datetime_helpers_test.dart new file mode 100644 index 0000000000..dfe83b4925 --- /dev/null +++ b/mobile/test/modules/utils/datetime_helpers_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; + +void main() { + group('tryFromSecondsSinceEpoch', () { + test('returns null for null input', () { + final result = tryFromSecondsSinceEpoch(null); + expect(result, isNull); + }); + + test('returns null for value below minimum allowed range', () { + // _minMillisecondsSinceEpoch = -62135596800000 + final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, isNull); + }); + + test('returns null for value above maximum allowed range', () { + // _maxMillisecondsSinceEpoch = 8640000000000000 + final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, isNull); + }); + + test('returns correct DateTime for minimum allowed value', () { + final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000)); + }); + + test('returns correct DateTime for maximum allowed value', () { + final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000)); + }); + + test('returns correct DateTime for negative timestamp', () { + final seconds = -1577836800; // Dec 31, 1919 (pre-epoch) + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000)); + }); + + test('returns correct DateTime for zero timestamp', () { + final seconds = 0; // Jan 1, 1970 (epoch) + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(0)); + }); + + test('returns correct DateTime for recent timestamp', () { + final now = DateTime.now(); + final seconds = now.millisecondsSinceEpoch ~/ 1000; + final result = tryFromSecondsSinceEpoch(seconds); + expect(result?.year, now.year); + expect(result?.month, now.month); + expect(result?.day, now.day); + }); + }); +} diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart new file mode 100644 index 0000000000..08ab1204a6 --- /dev/null +++ b/mobile/test/modules/utils/migration_test.dart @@ -0,0 +1,131 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/utils/migration.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../infrastructure/repository.mock.dart'; + +void main() { + late Drift db; + late SyncStreamRepository mockSyncStreamRepository; + + setUpAll(() async { + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + mockSyncStreamRepository = MockSyncStreamRepository(); + when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); + }); + + tearDown(() async { + await Store.clear(); + }); + + group('handleBetaMigration Tests', () { + group("version < 15", () { + test('already on new timeline', () async { + await Store.put(StoreKey.betaTimeline, true); + + await handleBetaMigration(14, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('already on old timeline', () async { + await Store.put(StoreKey.betaTimeline, false); + + await handleBetaMigration(14, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.needBetaMigration), true); + }); + + test('fresh install', () async { + await Store.delete(StoreKey.betaTimeline); + await handleBetaMigration(14, true, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + }); + + group("version == 15", () { + test('already on new timeline', () async { + await Store.put(StoreKey.betaTimeline, true); + + await handleBetaMigration(15, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('already on old timeline', () async { + await Store.put(StoreKey.betaTimeline, false); + + await handleBetaMigration(15, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.needBetaMigration), true); + }); + + test('fresh install', () async { + await Store.delete(StoreKey.betaTimeline); + await handleBetaMigration(15, true, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + }); + + group("version > 15", () { + test('already on new timeline', () async { + await Store.put(StoreKey.betaTimeline, true); + + await handleBetaMigration(16, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('already on old timeline', () async { + await Store.put(StoreKey.betaTimeline, false); + + await handleBetaMigration(16, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), false); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('fresh install', () async { + await Store.delete(StoreKey.betaTimeline); + await handleBetaMigration(16, true, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + }); + }); + + group('sync reset tests', () { + test('version < 16', () async { + await Store.put(StoreKey.shouldResetSync, false); + + await handleBetaMigration(15, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.shouldResetSync), true); + }); + + test('version >= 16', () async { + await Store.put(StoreKey.shouldResetSync, false); + + await handleBetaMigration(16, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.shouldResetSync), false); + }); + }); +} diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart index ba5d542fe6..1757826daf 100644 --- a/mobile/test/modules/utils/throttler_test.dart +++ b/mobile/test/modules/utils/throttler_test.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/utils/throttle.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class _Counter { int _count = 0; @@ -8,7 +8,7 @@ class _Counter { int get count => _count; void increment() { - debugPrint("Counter inside increment: $count"); + dPrint(() => "Counter inside increment: $count"); _count = _count + 1; } } diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index 3cb77c0b33..274176ae88 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -81,6 +81,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -110,6 +112,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -124,6 +128,8 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -141,6 +147,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -156,6 +164,8 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -171,6 +181,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -188,6 +200,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -203,6 +217,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -218,6 +234,8 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -233,6 +251,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -248,6 +268,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -265,6 +287,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -280,6 +304,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -295,6 +321,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -312,6 +340,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -327,6 +357,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -342,6 +374,8 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -359,6 +393,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -374,6 +410,8 @@ void main() { isTrashEnabled: false, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -391,6 +429,8 @@ void main() { isTrashEnabled: false, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -406,6 +446,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -423,6 +465,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -440,6 +484,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -457,6 +503,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -472,11 +520,29 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); expect(ActionButtonType.deleteLocal.shouldShow(context), isFalse); }); + + test('should show when asset is merged', () { + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.deleteLocal.shouldShow(context), isTrue); + }); }); group('upload button', () { @@ -489,6 +555,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -506,6 +574,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -520,6 +590,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -537,6 +609,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -552,6 +626,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -567,6 +643,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -581,12 +659,101 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); expect(ActionButtonType.likeActivity.shouldShow(context), isFalse); }); }); + + group('advancedTroubleshooting button', () { + test('should show when in advanced troubleshooting mode', () { + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: true, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.advancedInfo.shouldShow(context), isTrue); + }); + + test('should not show when not in advanced troubleshooting mode', () { + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.advancedInfo.shouldShow(context), isFalse); + }); + }); + }); + + group('unstack button', () { + test('should show when owner, not locked, has remote, and is stacked', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: true, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.unstack.shouldShow(context), isTrue); + }); + + test('should not show when not stacked', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.unstack.shouldShow(context), isFalse); + }); + + test('should not show when not owner', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: false, + isArchived: true, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.unstack.shouldShow(context), isFalse); + }); }); group('ActionButtonType.buildButton', () { @@ -602,6 +769,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); }); @@ -617,6 +786,23 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + final widget = buttonType.buildButton(contextWithAlbum); + expect(widget, isA()); + } else if (buttonType == ActionButtonType.unstack) { + final album = createRemoteAlbum(); + final contextWithAlbum = ActionButtonContext( + asset: asset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: true, source: ActionSource.timeline, ); final widget = buttonType.buildButton(contextWithAlbum); @@ -639,6 +825,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -658,6 +846,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -675,6 +865,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -693,6 +885,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); @@ -705,6 +899,8 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, source: ActionSource.timeline, ); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index eb9b6ac5a9..996eeef87e 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1855,7 +1855,7 @@ }, "/assets/bulk-upload-check": { "post": { - "description": "Checks if assets exist by checksums", + "description": "Checks if assets exist by checksums. This endpoint requires the `asset.upload` permission.", "operationId": "checkBulkUpload", "parameters": [], "requestBody": { @@ -1894,7 +1894,8 @@ "summary": "checkBulkUpload", "tags": [ "Assets" - ] + ], + "x-immich-permission": "asset.upload" } }, "/assets/device/{deviceId}": { @@ -2245,6 +2246,203 @@ "description": "This endpoint requires the `asset.update` permission." } }, + "/assets/{id}/metadata": { + "get": { + "operationId": "getAssetMetadata", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetMetadataResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Assets" + ], + "x-immich-permission": "asset.read", + "description": "This endpoint requires the `asset.read` permission." + }, + "put": { + "operationId": "updateAssetMetadata", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetMetadataUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetMetadataResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Assets" + ], + "x-immich-permission": "asset.update", + "description": "This endpoint requires the `asset.update` permission." + } + }, + "/assets/{id}/metadata/{key}": { + "delete": { + "operationId": "deleteAssetMetadata", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "key", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/AssetMetadataKey" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Assets" + ], + "x-immich-permission": "asset.update", + "description": "This endpoint requires the `asset.update` permission." + }, + "get": { + "operationId": "getAssetMetadataByKey", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "key", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/AssetMetadataKey" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetMetadataResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Assets" + ], + "x-immich-permission": "asset.read", + "description": "This endpoint requires the `asset.read` permission." + } + }, "/assets/{id}/original": { "get": { "operationId": "downloadAsset", @@ -2306,7 +2504,8 @@ "description": "This endpoint requires the `asset.download` permission." }, "put": { - "description": "Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.", + "deprecated": true, + "description": "This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.", "operationId": "replaceAsset", "parameters": [ { @@ -2368,12 +2567,14 @@ "api_key": [] } ], - "summary": "replaceAsset", + "summary": "Replace the asset with new file, without changing its id", "tags": [ - "Assets" + "Assets", + "Deprecated" ], "x-immich-lifecycle": { - "addedAt": "v1.106.0" + "addedAt": "v1.106.0", + "deprecatedAt": "v1.142.0" }, "x-immich-permission": "asset.replace" } @@ -4796,6 +4997,48 @@ ], "x-immich-permission": "partner.read", "description": "This endpoint requires the `partner.read` permission." + }, + "post": { + "operationId": "createPartner", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Partners" + ], + "x-immich-permission": "partner.create", + "description": "This endpoint requires the `partner.create` permission." } }, "/partners/{id}": { @@ -4835,7 +5078,9 @@ "description": "This endpoint requires the `partner.delete` permission." }, "post": { - "operationId": "createPartner", + "deprecated": true, + "description": "This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission.", + "operationId": "createPartnerDeprecated", "parameters": [ { "name": "id", @@ -4871,10 +5116,13 @@ } ], "tags": [ - "Partners" + "Partners", + "Deprecated" ], - "x-immich-permission": "partner.create", - "description": "This endpoint requires the `partner.create` permission." + "x-immich-lifecycle": { + "deprecatedAt": "v1.141.0" + }, + "x-immich-permission": "partner.create" }, "put": { "operationId": "updatePartner", @@ -4893,7 +5141,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdatePartnerDto" + "$ref": "#/components/schemas/PartnerUpdateDto" } } }, @@ -8650,6 +8898,15 @@ "$ref": "#/components/schemas/AssetVisibility" } }, + { + "name": "withCoordinates", + "required": false, + "in": "query", + "description": "Include location data in the response", + "schema": { + "type": "boolean" + } + }, { "name": "withPartners", "required": false, @@ -8795,6 +9052,15 @@ "$ref": "#/components/schemas/AssetVisibility" } }, + { + "name": "withCoordinates", + "required": false, + "in": "query", + "description": "Include location data in the response", + "schema": { + "type": "boolean" + } + }, { "name": "withPartners", "required": false, @@ -9592,7 +9858,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.139.4", + "version": "2.0.0", "contact": {} }, "tags": [], @@ -10615,6 +10881,12 @@ "format": "uuid", "type": "string" }, + "metadata": { + "items": { + "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" + }, + "type": "array" + }, "sidecarData": { "format": "binary", "type": "string" @@ -10632,7 +10904,8 @@ "deviceAssetId", "deviceId", "fileCreatedAt", - "fileModifiedAt" + "fileModifiedAt", + "metadata" ], "type": "object" }, @@ -10707,6 +10980,69 @@ ], "type": "string" }, + "AssetMetadataKey": { + "enum": [ + "mobile-app" + ], + "type": "string" + }, + "AssetMetadataResponseDto": { + "properties": { + "key": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetMetadataKey" + } + ] + }, + "updatedAt": { + "format": "date-time", + "type": "string" + }, + "value": { + "type": "object" + } + }, + "required": [ + "key", + "updatedAt", + "value" + ], + "type": "object" + }, + "AssetMetadataUpsertDto": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "AssetMetadataUpsertItemDto": { + "properties": { + "key": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetMetadataKey" + } + ] + }, + "value": { + "type": "object" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, "AssetOrder": { "enum": [ "asc", @@ -11923,6 +12259,25 @@ ], "type": "object" }, + "MachineLearningAvailabilityChecksDto": { + "properties": { + "enabled": { + "type": "boolean" + }, + "interval": { + "type": "number" + }, + "timeout": { + "type": "number" + } + }, + "required": [ + "enabled", + "interval", + "timeout" + ], + "type": "object" + }, "ManualJobName": { "enum": [ "person-cleanup", @@ -12585,6 +12940,18 @@ ], "type": "object" }, + "PartnerCreateDto": { + "properties": { + "sharedWithId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "sharedWithId" + ], + "type": "object" + }, "PartnerDirection": { "enum": [ "shared-by", @@ -12631,6 +12998,17 @@ ], "type": "object" }, + "PartnerUpdateDto": { + "properties": { + "inTimeline": { + "type": "boolean" + } + }, + "required": [ + "inTimeline" + ], + "type": "object" + }, "PeopleResponse": { "properties": { "enabled": { @@ -14303,6 +14681,10 @@ "query": { "type": "string" }, + "queryAssetId": { + "format": "uuid", + "type": "string" + }, "rating": { "maximum": 5, "minimum": -1, @@ -14370,9 +14752,6 @@ "type": "boolean" } }, - "required": [ - "query" - ], "type": "object" }, "SourceType": { @@ -14944,6 +15323,48 @@ ], "type": "object" }, + "SyncAssetMetadataDeleteV1": { + "properties": { + "assetId": { + "type": "string" + }, + "key": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetMetadataKey" + } + ] + } + }, + "required": [ + "assetId", + "key" + ], + "type": "object" + }, + "SyncAssetMetadataV1": { + "properties": { + "assetId": { + "type": "string" + }, + "key": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetMetadataKey" + } + ] + }, + "value": { + "type": "object" + } + }, + "required": [ + "assetId", + "key", + "value" + ], + "type": "object" + }, "SyncAssetV1": { "properties": { "checksum": { @@ -15106,6 +15527,10 @@ ], "type": "object" }, + "SyncCompleteV1": { + "properties": {}, + "type": "object" + }, "SyncEntityType": { "enum": [ "AuthUserV1", @@ -15114,6 +15539,8 @@ "AssetV1", "AssetDeleteV1", "AssetExifV1", + "AssetMetadataV1", + "AssetMetadataDeleteV1", "PartnerV1", "PartnerDeleteV1", "PartnerAssetV1", @@ -15151,7 +15578,8 @@ "UserMetadataV1", "UserMetadataDeleteV1", "SyncAckV1", - "SyncResetV1" + "SyncResetV1", + "SyncCompleteV1" ], "type": "string" }, @@ -15373,6 +15801,7 @@ "AlbumAssetExifsV1", "AssetsV1", "AssetExifsV1", + "AssetMetadataV1", "AuthUsersV1", "MemoriesV1", "MemoryToAssetsV1", @@ -15985,6 +16414,9 @@ }, "SystemConfigMachineLearningDto": { "properties": { + "availabilityChecks": { + "$ref": "#/components/schemas/MachineLearningAvailabilityChecksDto" + }, "clip": { "$ref": "#/components/schemas/CLIPConfig" }, @@ -15997,11 +16429,6 @@ "facialRecognition": { "$ref": "#/components/schemas/FacialRecognitionConfig" }, - "url": { - "deprecated": true, - "description": "This property was deprecated in v1.122.0", - "type": "string" - }, "urls": { "format": "uri", "items": { @@ -16013,6 +16440,7 @@ } }, "required": [ + "availabilityChecks", "clip", "duplicateDetection", "enabled", @@ -16677,6 +17105,14 @@ }, "type": "array" }, + "latitude": { + "description": "Array of latitude coordinates extracted from EXIF GPS data", + "items": { + "nullable": true, + "type": "number" + }, + "type": "array" + }, "livePhotoVideoId": { "description": "Array of live photo video asset IDs (null for non-live photos)", "items": { @@ -16692,6 +17128,14 @@ }, "type": "array" }, + "longitude": { + "description": "Array of longitude coordinates extracted from EXIF GPS data", + "items": { + "nullable": true, + "type": "number" + }, + "type": "array" + }, "ownerId": { "description": "Array of owner IDs for each asset", "items": { @@ -16922,17 +17366,6 @@ }, "type": "object" }, - "UpdatePartnerDto": { - "properties": { - "inTimeline": { - "type": "boolean" - } - }, - "required": [ - "inTimeline" - ], - "type": "object" - }, "UsageByUserDto": { "properties": { "photos": { diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 5b0b693c85..5497923a22 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.139.4", + "version": "2.0.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.17.1", + "@types/node": "^22.18.1", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "22.18.0" + "node": "22.20.0" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 08fa714823..ab885eb9c5 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.139.4 + * 2.0.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -447,6 +447,10 @@ export type AssetBulkDeleteDto = { force?: boolean; ids: string[]; }; +export type AssetMetadataUpsertItemDto = { + key: AssetMetadataKey; + value: object; +}; export type AssetMediaCreateDto = { assetData: Blob; deviceAssetId: string; @@ -457,6 +461,7 @@ export type AssetMediaCreateDto = { filename?: string; isFavorite?: boolean; livePhotoVideoId?: string; + metadata: AssetMetadataUpsertItemDto[]; sidecarData?: Blob; visibility?: AssetVisibility; }; @@ -516,6 +521,14 @@ export type UpdateAssetDto = { rating?: number; visibility?: AssetVisibility; }; +export type AssetMetadataResponseDto = { + key: AssetMetadataKey; + updatedAt: string; + value: object; +}; +export type AssetMetadataUpsertDto = { + items: AssetMetadataUpsertItemDto[]; +}; export type AssetMediaReplaceDto = { assetData: Blob; deviceAssetId: string; @@ -798,7 +811,10 @@ export type PartnerResponseDto = { profileChangedAt: string; profileImagePath: string; }; -export type UpdatePartnerDto = { +export type PartnerCreateDto = { + sharedWithId: string; +}; +export type PartnerUpdateDto = { inTimeline: boolean; }; export type PeopleResponseDto = { @@ -1001,7 +1017,8 @@ export type SmartSearchDto = { model?: string | null; page?: number; personIds?: string[]; - query: string; + query?: string; + queryAssetId?: string; rating?: number; size?: number; state?: string | null; @@ -1366,6 +1383,11 @@ export type SystemConfigLoggingDto = { enabled: boolean; level: LogLevel; }; +export type MachineLearningAvailabilityChecksDto = { + enabled: boolean; + interval: number; + timeout: number; +}; export type ClipConfig = { enabled: boolean; modelName: string; @@ -1382,12 +1404,11 @@ export type FacialRecognitionConfig = { modelName: string; }; export type SystemConfigMachineLearningDto = { + availabilityChecks: MachineLearningAvailabilityChecksDto; clip: ClipConfig; duplicateDetection: DuplicateDetectionConfig; enabled: boolean; facialRecognition: FacialRecognitionConfig; - /** This property was deprecated in v1.122.0 */ - url?: string; urls: string[]; }; export type SystemConfigMapDto = { @@ -1544,10 +1565,14 @@ export type TimeBucketAssetResponseDto = { isImage: boolean[]; /** Array indicating whether each asset is in the trash */ isTrashed: boolean[]; + /** Array of latitude coordinates extracted from EXIF GPS data */ + latitude?: (number | null)[]; /** Array of live photo video asset IDs (null for non-live photos) */ livePhotoVideoId: (string | null)[]; /** Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. */ localOffsetHours: number[]; + /** Array of longitude coordinates extracted from EXIF GPS data */ + longitude?: (number | null)[]; /** Array of owner IDs for each asset */ ownerId: string[]; /** Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL") */ @@ -2273,6 +2298,61 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } +/** + * This endpoint requires the `asset.read` permission. + */ +export function getAssetMetadata({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetMetadataResponseDto[]; + }>(`/assets/${encodeURIComponent(id)}/metadata`, { + ...opts + })); +} +/** + * This endpoint requires the `asset.update` permission. + */ +export function updateAssetMetadata({ id, assetMetadataUpsertDto }: { + id: string; + assetMetadataUpsertDto: AssetMetadataUpsertDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetMetadataResponseDto[]; + }>(`/assets/${encodeURIComponent(id)}/metadata`, oazapfts.json({ + ...opts, + method: "PUT", + body: assetMetadataUpsertDto + }))); +} +/** + * This endpoint requires the `asset.update` permission. + */ +export function deleteAssetMetadata({ id, key }: { + id: string; + key: AssetMetadataKey; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/metadata/${encodeURIComponent(key)}`, { + ...opts, + method: "DELETE" + })); +} +/** + * This endpoint requires the `asset.read` permission. + */ +export function getAssetMetadataByKey({ id, key }: { + id: string; + key: AssetMetadataKey; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetMetadataResponseDto; + }>(`/assets/${encodeURIComponent(id)}/metadata/${encodeURIComponent(key)}`, { + ...opts + })); +} /** * This endpoint requires the `asset.download` permission. */ @@ -2292,7 +2372,7 @@ export function downloadAsset({ id, key, slug }: { })); } /** - * replaceAsset + * Replace the asset with new file, without changing its id */ export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: { id: string; @@ -3053,6 +3133,21 @@ export function getPartners({ direction }: { ...opts })); } +/** + * This endpoint requires the `partner.create` permission. + */ +export function createPartner({ partnerCreateDto }: { + partnerCreateDto: PartnerCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: PartnerResponseDto; + }>("/partners", oazapfts.json({ + ...opts, + method: "POST", + body: partnerCreateDto + }))); +} /** * This endpoint requires the `partner.delete` permission. */ @@ -3065,9 +3160,9 @@ export function removePartner({ id }: { })); } /** - * This endpoint requires the `partner.create` permission. + * This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. */ -export function createPartner({ id }: { +export function createPartnerDeprecated({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ @@ -3081,9 +3176,9 @@ export function createPartner({ id }: { /** * This endpoint requires the `partner.update` permission. */ -export function updatePartner({ id, updatePartnerDto }: { +export function updatePartner({ id, partnerUpdateDto }: { id: string; - updatePartnerDto: UpdatePartnerDto; + partnerUpdateDto: PartnerUpdateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3091,7 +3186,7 @@ export function updatePartner({ id, updatePartnerDto }: { }>(`/partners/${encodeURIComponent(id)}`, oazapfts.json({ ...opts, method: "PUT", - body: updatePartnerDto + body: partnerUpdateDto }))); } /** @@ -4206,7 +4301,7 @@ export function tagAssets({ id, bulkIdsDto }: { /** * This endpoint requires the `asset.read` permission. */ -export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withPartners, withStacked }: { +export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: { albumId?: string; isFavorite?: boolean; isTrashed?: boolean; @@ -4218,6 +4313,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers timeBucket: string; userId?: string; visibility?: AssetVisibility; + withCoordinates?: boolean; withPartners?: boolean; withStacked?: boolean; }, opts?: Oazapfts.RequestOpts) { @@ -4236,6 +4332,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers timeBucket, userId, visibility, + withCoordinates, withPartners, withStacked }))}`, { @@ -4245,7 +4342,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers /** * This endpoint requires the `asset.read` permission. */ -export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: { albumId?: string; isFavorite?: boolean; isTrashed?: boolean; @@ -4256,6 +4353,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per tagId?: string; userId?: string; visibility?: AssetVisibility; + withCoordinates?: boolean; withPartners?: boolean; withStacked?: boolean; }, opts?: Oazapfts.RequestOpts) { @@ -4273,6 +4371,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per tagId, userId, visibility, + withCoordinates, withPartners, withStacked }))}`, { @@ -4725,6 +4824,9 @@ export enum Permission { AdminUserDelete = "adminUser.delete", AdminAuthUnlinkAll = "adminAuth.unlinkAll" } +export enum AssetMetadataKey { + MobileApp = "mobile-app" +} export enum AssetMediaStatus { Created = "created", Replaced = "replaced", @@ -4811,6 +4913,8 @@ export enum SyncEntityType { AssetV1 = "AssetV1", AssetDeleteV1 = "AssetDeleteV1", AssetExifV1 = "AssetExifV1", + AssetMetadataV1 = "AssetMetadataV1", + AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", PartnerV1 = "PartnerV1", PartnerDeleteV1 = "PartnerDeleteV1", PartnerAssetV1 = "PartnerAssetV1", @@ -4848,7 +4952,8 @@ export enum SyncEntityType { UserMetadataV1 = "UserMetadataV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1", SyncAckV1 = "SyncAckV1", - SyncResetV1 = "SyncResetV1" + SyncResetV1 = "SyncResetV1", + SyncCompleteV1 = "SyncCompleteV1" } export enum SyncRequestType { AlbumsV1 = "AlbumsV1", @@ -4858,6 +4963,7 @@ export enum SyncRequestType { AlbumAssetExifsV1 = "AlbumAssetExifsV1", AssetsV1 = "AssetsV1", AssetExifsV1 = "AssetExifsV1", + AssetMetadataV1 = "AssetMetadataV1", AuthUsersV1 = "AuthUsersV1", MemoriesV1 = "MemoriesV1", MemoryToAssetsV1 = "MemoryToAssetsV1", diff --git a/package.json b/package.json index a718d7ccd0..18610fe4bc 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748", + "packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67", "engines": { "pnpm": ">=10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa24e5b31f..75e1e5808d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: overrides: canvas: 2.11.2 - sharp: ^0.34.2 + sharp: ^0.34.3 packageExtensionsChecksum: sha256-DAYr0FTkvKYnvBH4muAER9UE1FVGKhqfRU4/QwA2xPQ= @@ -41,12 +41,9 @@ importers: specifier: ^4.0.8 version: 4.0.8 devDependencies: - '@eslint/eslintrc': - specifier: ^3.1.0 - version: 3.3.1 '@eslint/js': specifier: ^9.8.0 - version: 9.33.0 + version: 9.35.0 '@immich/sdk': specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk @@ -66,11 +63,11 @@ importers: specifier: ^4.13.1 version: 4.13.4 '@types/node': - specifier: ^22.17.1 - version: 22.17.2 + specifier: ^22.18.1 + version: 22.18.5 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -82,19 +79,19 @@ importers: version: 12.1.0 eslint: specifier: ^9.14.0 - version: 9.33.0(jiti@2.5.1) + version: 9.35.0(jiti@2.5.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.33.0(jiti@2.5.1)) + version: 10.1.8(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(prettier@3.6.2) eslint-plugin-unicorn: specifier: ^60.0.0 - version: 60.0.0(eslint@9.33.0(jiti@2.5.1)) + version: 60.0.0(eslint@9.35.0(jiti@2.5.1)) globals: specifier: ^16.0.0 - version: 16.3.0 + version: 16.4.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -109,19 +106,19 @@ importers: version: 5.9.2 typescript-eslint: specifier: ^8.28.0 - version: 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + version: 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) vite: specifier: ^7.0.0 - version: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) yaml: specifier: ^2.3.1 version: 2.8.1 @@ -130,13 +127,13 @@ importers: dependencies: '@docusaurus/core': specifier: ~3.8.0 - version: 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + version: 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/preset-classic': specifier: ~3.8.0 - version: 3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) + version: 3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(@types/react@19.1.13)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) '@docusaurus/theme-common': specifier: ~3.8.0 - version: 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -145,22 +142,13 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.0(@types/react@19.1.10)(react@18.3.1) + version: 3.1.1(@types/react@19.1.13)(react@18.3.1) autoprefixer: specifier: ^10.4.17 version: 10.4.21(postcss@8.5.6) - classnames: - specifier: ^2.3.2 - version: 2.5.1 - clsx: - specifier: ^2.0.0 - version: 2.1.1 docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - docusaurus-preset-openapi: - specifier: ^0.7.5 - version: 0.7.6(@algolia/client-search@5.29.0)(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)(search-insights@2.17.3)(typescript@5.9.2) + version: 3.6.0(@docusaurus/core@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lunr: specifier: ^2.3.9 version: 2.3.9 @@ -172,7 +160,7 @@ importers: version: 2.4.1(react@18.3.1) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.99.9) + version: 4.0.2(webpack@5.100.2) react: specifier: ^18.0.0 version: 18.3.1 @@ -204,12 +192,9 @@ importers: e2e: devDependencies: - '@eslint/eslintrc': - specifier: ^3.1.0 - version: 3.3.1 '@eslint/js': specifier: ^9.8.0 - version: 9.33.0 + version: 9.35.0 '@immich/cli': specifier: file:../cli version: link:../cli @@ -218,7 +203,7 @@ importers: version: link:../open-api/typescript-sdk '@playwright/test': specifier: ^1.44.1 - version: 1.54.2 + version: 1.55.0 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 @@ -226,11 +211,11 @@ importers: specifier: ^3.4.2 version: 3.7.1 '@types/node': - specifier: ^22.17.1 - version: 22.17.2 + specifier: ^22.18.1 + version: 22.18.5 '@types/oidc-provider': specifier: ^9.0.0 - version: 9.1.2 + version: 9.5.0 '@types/pg': specifier: ^8.15.1 version: 8.15.5 @@ -240,36 +225,33 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.3 - '@vitest/coverage-v8': - specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) eslint: specifier: ^9.14.0 - version: 9.33.0(jiti@2.5.1) + version: 9.35.0(jiti@2.5.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.33.0(jiti@2.5.1)) + version: 10.1.8(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(prettier@3.6.2) eslint-plugin-unicorn: specifier: ^60.0.0 - version: 60.0.0(eslint@9.33.0(jiti@2.5.1)) + version: 60.0.0(eslint@9.35.0(jiti@2.5.1)) exiftool-vendored: specifier: ^28.3.1 version: 28.8.0 globals: specifier: ^16.0.0 - version: 16.3.0 + version: 16.4.0 jose: specifier: ^5.6.3 version: 5.10.0 luxon: specifier: ^3.4.4 - version: 3.7.1 + version: 3.7.2 oidc-provider: specifier: ^9.0.0 - version: 9.4.1 + version: 9.5.1 pg: specifier: ^8.11.3 version: 8.16.3 @@ -283,8 +265,8 @@ importers: specifier: ^4.0.0 version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) sharp: - specifier: ^0.34.2 - version: 0.34.2 + specifier: ^0.34.3 + version: 0.34.3 socket.io-client: specifier: ^4.7.4 version: 4.8.1 @@ -296,13 +278,13 @@ importers: version: 5.9.2 typescript-eslint: specifier: ^8.28.0 - version: 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + version: 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) utimes: specifier: ^5.2.1 version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) open-api/typescript-sdk: dependencies: @@ -311,8 +293,8 @@ importers: version: 1.0.4 devDependencies: '@types/node': - specifier: ^22.17.1 - version: 22.17.2 + specifier: ^22.18.1 + version: 22.18.5 typescript: specifier: ^5.3.3 version: 5.9.2 @@ -321,16 +303,13 @@ importers: dependencies: '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.57.0) + version: 11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.58.5) '@nestjs/common': specifier: ^11.0.4 version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.4 version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter': - specifier: ^3.0.0 - version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) '@nestjs/platform-express': specifier: ^11.0.4 version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) @@ -349,45 +328,42 @@ importers: '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 - '@opentelemetry/auto-instrumentations-node': - specifier: ^0.62.0 - version: 0.62.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.0.1(@opentelemetry/api@1.9.0) + version: 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) + specifier: ^0.205.0 + version: 0.205.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) + specifier: ^0.205.0 + version: 0.205.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': + specifier: ^0.53.0 + version: 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': specifier: ^0.51.0 version: 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.49.0 - version: 0.49.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.56.0 - version: 0.56.0(@opentelemetry/api@1.9.0) + specifier: ^0.58.0 + version: 0.58.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.0.1(@opentelemetry/api@1.9.0) + version: 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.0.1(@opentelemetry/api@1.9.0) + version: 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) + specifier: ^0.205.0 + version: 0.205.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 - version: 1.36.0 + version: 1.37.0 '@react-email/components': specifier: ^0.5.0 - version: 0.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 0.5.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-email/render': specifier: ^1.1.2 - version: 1.2.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.5) @@ -405,7 +381,7 @@ importers: version: 2.2.0 bullmq: specifier: ^5.51.0 - version: 5.57.0 + version: 5.58.5 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -465,7 +441,7 @@ importers: version: 4.17.21 luxon: specifier: ^3.4.2 - version: 3.7.1 + version: 3.7.2 mnemonist: specifier: ^0.40.3 version: 0.40.3 @@ -474,7 +450,7 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.18.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@types/inquirer@8.2.11)(typescript@5.9.2) + version: 3.19.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@types/inquirer@8.2.11)(@types/node@22.18.5)(typescript@5.9.2) nestjs-cls: specifier: ^5.0.0 version: 5.4.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -486,10 +462,10 @@ importers: version: 7.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) nodemailer: specifier: ^7.0.0 - version: 7.0.5 + version: 7.0.6 openid-client: specifier: ^6.3.3 - version: 6.6.4 + version: 6.8.0 pg: specifier: ^8.11.3 version: 8.16.3 @@ -510,7 +486,7 @@ importers: version: 19.1.1(react@19.1.1) react-email: specifier: ^4.0.0 - version: 4.2.8 + version: 4.2.11 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -527,11 +503,11 @@ importers: specifier: ^7.6.2 version: 7.7.2 sharp: - specifier: ^0.34.2 - version: 0.34.2 + specifier: ^0.34.3 + version: 0.34.3 sirv: specifier: ^3.0.0 - version: 3.0.1 + version: 3.0.2 socket.io: specifier: ^4.8.1 version: 4.8.1 @@ -541,12 +517,9 @@ importers: thumbhash: specifier: ^0.1.1 version: 0.1.1 - typeorm: - specifier: ^0.3.17 - version: 0.3.25(ioredis@5.7.0)(pg@8.16.3)(reflect-metadata@0.2.2) ua-parser-js: specifier: ^2.0.0 - version: 2.0.4(encoding@0.1.13) + version: 2.0.5 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -554,15 +527,12 @@ importers: specifier: ^13.12.0 version: 13.15.15 devDependencies: - '@eslint/eslintrc': - specifier: ^3.1.0 - version: 3.3.1 '@eslint/js': specifier: ^9.8.0 - version: 9.33.0 + version: 9.35.0 '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.10(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/node@22.13.14) + version: 11.0.10(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.5) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.7(chokidar@4.0.3)(typescript@5.9.2) @@ -571,13 +541,7 @@ importers: version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6) '@swc/core': specifier: ^1.4.14 - version: 1.13.3(@swc/helpers@0.5.17) - '@testcontainers/postgresql': - specifier: ^11.0.0 - version: 11.5.1 - '@testcontainers/redis': - specifier: ^11.0.0 - version: 11.5.1 + version: 1.13.5(@swc/helpers@0.5.17) '@types/archiver': specifier: ^6.0.0 version: 6.0.3 @@ -618,11 +582,11 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@types/node': - specifier: ^22.13.14 - version: 22.13.14 + specifier: ^22.18.1 + version: 22.18.5 '@types/nodemailer': - specifier: ^6.4.14 - version: 6.4.17 + specifier: ^7.0.0 + version: 7.0.1 '@types/picomatch': specifier: ^4.0.0 version: 4.0.2 @@ -631,13 +595,13 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.1.10 + version: 19.1.13 '@types/sanitize-html': specifier: ^2.13.0 version: 2.16.0 '@types/semver': specifier: ^7.5.8 - version: 7.7.0 + version: 7.7.1 '@types/supertest': specifier: ^6.0.0 version: 6.0.3 @@ -646,37 +610,31 @@ importers: version: 0.7.39 '@types/validator': specifier: ^13.15.2 - version: 13.15.2 + version: 13.15.3 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) - canvas: - specifier: 2.11.2 - version: 2.11.2(encoding@0.1.13) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) eslint: specifier: ^9.14.0 - version: 9.33.0(jiti@2.5.1) + version: 9.35.0(jiti@2.5.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.33.0(jiti@2.5.1)) + version: 10.1.8(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(prettier@3.6.2) eslint-plugin-unicorn: specifier: ^60.0.0 - version: 60.0.0(eslint@9.33.0(jiti@2.5.1)) + version: 60.0.0(eslint@9.35.0(jiti@2.5.1)) globals: specifier: ^16.0.0 - version: 16.3.0 + version: 16.4.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 - node-addon-api: - specifier: ^8.3.1 - version: 8.5.0 node-gyp: specifier: ^11.2.0 - version: 11.3.0 + version: 11.4.2 pngjs: specifier: ^7.0.0 version: 7.0.0 @@ -686,15 +644,9 @@ importers: prettier-plugin-organize-imports: specifier: ^4.0.0 version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) - rimraf: - specifier: ^6.0.0 - version: 6.0.1 - source-map-support: - specifier: ^0.5.21 - version: 0.5.21 sql-formatter: specifier: ^15.0.0 - version: 15.6.6 + version: 15.6.9 supertest: specifier: ^7.1.0 version: 7.1.4 @@ -704,27 +656,21 @@ importers: testcontainers: specifier: ^11.0.0 version: 11.5.1 - tsconfig-paths: - specifier: ^4.2.0 - version: 4.2.0 typescript: specifier: ^5.9.2 version: 5.9.2 typescript-eslint: specifier: ^8.28.0 - version: 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + version: 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) unplugin-swc: specifier: ^1.4.5 - version: 1.5.5(@swc/core@1.13.3(@swc/helpers@0.5.17))(rollup@4.46.3) - utimes: - specifier: ^5.2.1 - version: 5.2.1(encoding@0.1.13) + version: 1.5.7(@swc/core@1.13.5(@swc/helpers@0.5.17))(rollup@4.50.1) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.2)(vite@7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) web: dependencies: @@ -735,8 +681,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.24.0 - version: 0.24.1(@internationalized/date@3.8.2)(svelte@5.35.5) + specifier: ^0.29.0 + version: 0.29.0(@internationalized/date@3.8.2)(svelte@5.38.10) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -745,19 +691,19 @@ importers: version: 7.4.47 '@photo-sphere-viewer/core': specifier: ^5.11.5 - version: 5.13.4 + version: 5.14.0 '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.11.5 - version: 5.13.4(@photo-sphere-viewer/core@5.13.4)(@photo-sphere-viewer/video-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4)) + version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.11.5 - version: 5.13.4(@photo-sphere-viewer/core@5.13.4)(@photo-sphere-viewer/settings-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4)) + version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.11.5 - version: 5.13.4(@photo-sphere-viewer/core@5.13.4) + version: 5.14.0(@photo-sphere-viewer/core@5.14.0) '@photo-sphere-viewer/video-plugin': specifier: ^5.11.5 - version: 5.13.4(@photo-sphere-viewer/core@5.13.4) + version: 5.14.0(@photo-sphere-viewer/core@5.14.0) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -766,7 +712,7 @@ importers: version: 0.41.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.4(svelte@5.35.5) + version: 0.3.4(svelte@5.38.10) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -799,31 +745,34 @@ importers: version: 4.17.21 luxon: specifier: ^3.4.4 - version: 3.7.1 + version: 3.7.2 maplibre-gl: specifier: ^5.6.2 - version: 5.6.2 + version: 5.7.1 pmtiles: specifier: ^4.3.0 version: 4.3.0 qrcode: specifier: ^1.5.4 version: 1.5.4 + simple-icons: + specifier: ^15.15.0 + version: 15.15.0 socket.io-client: specifier: ~4.8.0 version: 4.8.1 svelte-gestures: - specifier: ^5.1.3 - version: 5.1.4 + specifier: ^5.2.2 + version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.35.5) + version: 4.0.1(svelte@5.38.10) svelte-maplibre: specifier: ^1.2.0 - version: 1.2.0(svelte@5.35.5) + version: 1.2.1(svelte@5.38.10) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.35.5) + version: 0.12.0(svelte@5.38.10) tabbable: specifier: ^6.2.0 version: 6.2.0 @@ -831,42 +780,39 @@ importers: specifier: ^0.1.1 version: 0.1.1 devDependencies: - '@eslint/eslintrc': - specifier: ^3.1.0 - version: 3.3.1 '@eslint/js': specifier: ^9.18.0 - version: 9.33.0 + version: 9.35.0 '@faker-js/faker': - specifier: ^9.3.0 - version: 9.9.0 + specifier: ^10.0.0 + version: 10.0.0 '@koddsson/eslint-plugin-tscompat': specifier: ^0.2.0 - version: 0.2.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) + version: 0.2.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) '@socket.io/component-emitter': specifier: ^3.1.0 version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.9(@sveltejs/kit@2.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))) + version: 3.0.9(@sveltejs/kit@2.38.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))) '@sveltejs/enhanced-img': specifier: ^0.8.0 - version: 0.8.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(rollup@4.46.3)(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 0.8.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(rollup@4.50.1)(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 2.38.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@sveltejs/vite-plugin-svelte': - specifier: 6.1.2 - version: 6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + specifier: 6.2.0 + version: 6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.12(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 4.1.13(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@testing-library/jest-dom': specifier: ^6.4.2 - version: 6.7.0 + version: 6.8.0 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.8(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + version: 5.2.8(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.0) @@ -890,79 +836,73 @@ importers: version: 1.5.5 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) - autoprefixer: - specifier: ^10.4.17 - version: 10.4.21(postcss@8.5.6) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) dotenv: specifier: ^17.0.0 - version: 17.2.1 + version: 17.2.2 eslint: specifier: ^9.18.0 - version: 9.33.0(jiti@2.5.1) + version: 9.35.0(jiti@2.5.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.33.0(jiti@2.5.1)) + version: 10.1.8(eslint@9.35.0(jiti@2.5.1)) eslint-p: - specifier: ^0.25.0 - version: 0.25.0(jiti@2.5.1) + specifier: ^0.26.0 + version: 0.26.0(jiti@2.5.1) eslint-plugin-compat: specifier: ^6.0.2 - version: 6.0.2(eslint@9.33.0(jiti@2.5.1)) + version: 6.0.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-svelte: specifier: ^3.9.0 - version: 3.11.0(eslint@9.33.0(jiti@2.5.1))(svelte@5.35.5) + version: 3.12.3(eslint@9.35.0(jiti@2.5.1))(svelte@5.38.10) eslint-plugin-unicorn: specifier: ^60.0.0 - version: 60.0.0(eslint@9.33.0(jiti@2.5.1)) + version: 60.0.0(eslint@9.35.0(jiti@2.5.1)) factory.ts: specifier: ^1.4.1 version: 1.4.2 globals: specifier: ^16.0.0 - version: 16.3.0 + version: 16.4.0 prettier: specifier: ^3.4.2 version: 3.6.2 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.2.0(prettier@3.6.2)(typescript@5.8.3) + version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) prettier-plugin-sort-json: specifier: ^4.1.1 version: 4.1.1(prettier@3.6.2) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.0(prettier@3.6.2)(svelte@5.35.5) + version: 3.4.0(prettier@3.6.2)(svelte@5.38.10) rollup-plugin-visualizer: specifier: ^6.0.0 - version: 6.0.3(rollup@4.46.3) + version: 6.0.3(rollup@4.50.1) svelte: - specifier: 5.35.5 - version: 5.35.5 + specifier: 5.38.10 + version: 5.38.10 svelte-check: specifier: ^4.1.5 - version: 4.3.1(picomatch@4.0.3)(svelte@5.35.5)(typescript@5.8.3) + version: 4.3.1(picomatch@4.0.3)(svelte@5.38.10)(typescript@5.9.2) svelte-eslint-parser: specifier: ^1.2.0 - version: 1.3.1(svelte@5.35.5) + version: 1.3.2(svelte@5.38.10) tailwindcss: specifier: ^4.1.7 - version: 4.1.12 - tslib: - specifier: ^2.6.2 - version: 2.8.1 + version: 4.1.13 typescript: specifier: ^5.8.3 - version: 5.8.3 + version: 5.9.2 typescript-eslint: specifier: ^8.28.0 - version: 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) + version: 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) vite: specifier: ^7.1.2 - version: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) packages: @@ -1073,6 +1013,131 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-sesv2@3.890.0': + resolution: {integrity: sha512-AM9Lt4QkNet8xKgCwJxfkyqlorwG9S+tvtpSfHYCVq0j2Z6PbkDaUBnvwjGOMBV7Um5IzZ7yhvQXrBg7omZciQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.890.0': + resolution: {integrity: sha512-vefYNwh/K5V5YiJpFJfoMPNqsoiRTqD7ZnkvR0cjJdwhOIwFnSKN1vz0OMjySTQmVMcG4JKGVul82ou7ErtOhQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.890.0': + resolution: {integrity: sha512-CT+yjhytHdyKvV3Nh/fqBjnZ8+UiQZVz4NMm4LrPATgVSOdfygXHqrWxrPTVgiBtuJWkotg06DF7+pTd5ekLBw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.890.0': + resolution: {integrity: sha512-BtsUa2y0Rs8phmB2ScZ5RuPqZVmxJJXjGfeiXctmLFTxTwoayIK1DdNzOWx6SRMPVc3s2RBGN4vO7T1TwN+ajA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.890.0': + resolution: {integrity: sha512-0sru3LVwsuGYyzbD90EC/d5HnCZ9PL4O9BA2LYT6b9XceC005Oj86uzE47LXb+mDhTAt3T6ZO0+ZcVQe0DDi8w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.890.0': + resolution: {integrity: sha512-Mxv7ByftHKH7dE6YXu9gQ6ODXwO1iSO32t8tBrZLS3g8K1knWADIqDFv3yErQtJ8hp27IDxbAbVH/1RQdSkmhA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.890.0': + resolution: {integrity: sha512-zbPz3mUtaBdch0KoH8/LouRDcYSzyT2ecyCOo5OAFVil7AxT1jvsn4vX78FlnSVpZ4mLuHY8pHTVGi235XiyBA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.890.0': + resolution: {integrity: sha512-dWZ54TI1Q+UerF5YOqGiCzY+x2YfHsSQvkyM3T4QDNTJpb/zjiVv327VbSOULOlI7gHKWY/G3tMz0D9nWI7YbA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.890.0': + resolution: {integrity: sha512-ajYCZ6f2+98w8zG/IXcQ+NhWYoI5qPUDovw+gMqMWX/jL1cmZ9PFAwj2Vyq9cbjum5RNWwPLArWytTCgJex4AQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.890.0': + resolution: {integrity: sha512-qZ2Mx7BeYR1s0F/H6wePI0MAmkFswmBgrpgMCOt2S4b2IpQPnUa2JbxY3GwW2WqX3nV0KjPW08ctSLMmlq/tKA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.887.0': + resolution: {integrity: sha512-ulzqXv6NNqdu/kr0sgBYupWmahISHY+azpJidtK6ZwQIC+vBUk9NdZeqQpy7KVhIk2xd4+5Oq9rxapPwPI21CA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.887.0': + resolution: {integrity: sha512-YbbgLI6jKp2qSoAcHnXrQ5jcuc5EYAmGLVFgMVdk8dfCfJLfGGSaOLxF4CXC7QYhO50s+mPPkhBYejCik02Kug==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.887.0': + resolution: {integrity: sha512-tjrUXFtQnFLo+qwMveq5faxP5MQakoLArXtqieHphSqZTXm21wDJM73hgT4/PQQGTwgYjDKqnqsE1hvk0hcfDw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.890.0': + resolution: {integrity: sha512-58P1lrE606zpp29xH9Keh3j2BWfa2ciGBtygJTpulRMlqPL3U1gFfU2g5nDYJbjKgRtCgNIBqfmtkL4eikCb9w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.890.0': + resolution: {integrity: sha512-x4+gLrOFGN7PnfxCaQbs3QEF8bMQE4CVxcOp066UEJqr2Pn4yB12Q3O+YntOtESK5NcTxIh7JlhGss95EHzNng==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.890.0': + resolution: {integrity: sha512-D5qVNd+qlqdL8duJShzffAqPllGRA4tG7n/GEpL13eNfHChPvGkkUFBMrxSgCAETaTna13G6kq+dMO+SAdbm1A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.890.0': + resolution: {integrity: sha512-VfdT+tkF9groRYNzKvQCsCGDbOQdeBdzyB1d6hWiq22u13UafMIoskJ1ec0i0H1X29oT6mjTitfnvPq1UiKwzQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.890.0': + resolution: {integrity: sha512-il8kb2/wDLXhemN3p7v4MvbvqoMuo7Ug3ihuIUIhPtSVjcnn+BISJU0S+5YTl8TXf6qxML9VrfxL0pmuhO3BsA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.890.0': + resolution: {integrity: sha512-+pK/0iQEpPmnztbAw0NNmb+B5pPy8VLu+Ab4SJLgVp41RE9NO13VQtrzUbh61TTAVMrzqWlLQ2qmAl2Fk4VNgw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.887.0': + resolution: {integrity: sha512-fmTEJpUhsPsovQ12vZSpVTEP/IaRoJAMBGQXlQNjtCpkBp6Iq3KQDa/HDaPINE+3xxo6XvTdtibsNOd5zJLV9A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-arn-parser@3.873.0': + resolution: {integrity: sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.890.0': + resolution: {integrity: sha512-nJ8v1x9ZQKzMRK4dS4oefOMIHqb6cguctTcx1RB9iTaFOR5pP7bvq+D4mvNZ6vBxiHg1dQGBUUgl5XJmdR7atQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.873.0': + resolution: {integrity: sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.887.0': + resolution: {integrity: sha512-X71UmVsYc6ZTH4KU6hA5urOzYowSXc3qvroagJNLJYU1ilgZ529lP4J9XOYfEvTXkLR1hPFSRxa43SrwgelMjA==} + + '@aws-sdk/util-user-agent-node@3.890.0': + resolution: {integrity: sha512-s85NkCxKoAlUvx7UP7OelxLqwTi27Tps9/Q+4N+9rEUjThxEnDsqJSStJ1XiYhddz1xc/vxMvPjYN0qX6EKPtA==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.887.0': + resolution: {integrity: sha512-lMwgWK1kNgUhHGfBvO/5uLe7TKhycwOn3eRCqsKPT9aPCx/HWuTlpcQp8oW2pCRGLS7qzcxqpQulcD+bbUL7XQ==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.0.1': + resolution: {integrity: sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1085,10 +1150,6 @@ packages: resolution: {integrity: sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.5': - resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} @@ -1180,13 +1241,8 @@ packages: resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.7': - resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true @@ -1630,32 +1686,20 @@ packages: resolution: {integrity: sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.28.3': - resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.7': - resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.7': - resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} '@balena/dockerignore@1.0.2': @@ -1679,8 +1723,8 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} '@csstools/css-calc@2.1.4': @@ -1690,8 +1734,8 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.10': - resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 @@ -2124,8 +2168,8 @@ packages: resolution: {integrity: sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==} engines: {node: '>=18.0'} - '@emnapi/runtime@1.4.5': - resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -2421,8 +2465,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -2439,14 +2483,6 @@ packages: resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.15.1': - resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.2': resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2455,36 +2491,21 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.30.1': - resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.33.0': - resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + '@eslint/js@9.35.0': + resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.3': - resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.5': resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@exodus/schemasafe@1.3.0': - resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - - '@faker-js/faker@5.5.3': - resolution: {integrity: sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==} - deprecated: Please update to a newer version. - - '@faker-js/faker@9.9.0': - resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} - engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@faker-js/faker@10.0.0': + resolution: {integrity: sha512-UollFEUkVXutsaP+Vndjxar40Gs5JL2HeLcl8xO1QAjJgOdhc3OmBFWyEylS+RddWaaBiAzH+5/17PLQJwDiLw==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@fig/complete-commander@3.2.0': resolution: {integrity: sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==} @@ -2494,8 +2515,8 @@ packages: '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.7.3': - resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} @@ -2540,140 +2561,142 @@ packages: resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.34.2': - resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.2': - resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.1.0': - resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.1.0': - resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.1.0': - resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.1.0': - resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.1.0': - resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.1.0': - resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.1.0': - resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': - resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.1.0': - resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.2': - resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.2': - resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.34.2': - resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.2': - resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.2': - resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.2': - resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.2': - resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.2': - resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==} + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.2': - resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.2': - resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] - '@immich/ui@0.24.1': - resolution: {integrity: sha512-phJ9BHV0+OnKsxXD+5+Te5Amnb1N4ExYpRGSJPYFqutd5WXeN7kZGKZXd3CfcQ1e31SXRy4DsHSGdM1pY7AUgA==} + '@immich/ui@0.29.0': + resolution: {integrity: sha512-An9cf1L4nMO6+C1Tkktd+qjGmZvyGz/Un33cGsKQa2I7IdZHd67KbbC2v3wN3bQMiTjxtFJ8YR9EONohJ8jDtQ==} peerDependencies: svelte: ^5.0.0 @@ -2722,8 +2745,8 @@ packages: '@types/node': optional: true - '@inquirer/external-editor@1.0.1': - resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==} + '@inquirer/external-editor@1.0.2': + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2853,10 +2876,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -2864,22 +2883,12 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} @@ -2979,8 +2988,8 @@ packages: '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} - '@mdx-js/react@3.1.0': - resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: '@types/react': '>=16' react: '>=16' @@ -2988,16 +2997,6 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} - '@monaco-editor/loader@1.5.0': - resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} - - '@monaco-editor/react@4.7.0': - resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} - peerDependencies: - monaco-editor: '>= 0.25.0 < 1' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -3088,12 +3087,6 @@ packages: '@nestjs/websockets': optional: true - '@nestjs/event-emitter@3.0.1': - resolution: {integrity: sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==} - peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 - '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/mapped-types@2.1.0': resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} peerDependencies: @@ -3205,95 +3198,88 @@ packages: '@oazapfts/runtime@1.0.4': resolution: {integrity: sha512-7t6C2shug/6tZhQgkCa532oTYBLEnbASV/i1SG1rH2GB4h3aQQujYciYSPT92hvN4IwTe8S2hPkN/6iiOyTlCg==} - '@opentelemetry/api-logs@0.203.0': - resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} + '@opentelemetry/api-logs@0.205.0': + resolution: {integrity: sha512-wBlPk1nFB37Hsm+3Qy73yQSobVn28F4isnWIBvKpd5IUH/eat8bwcL02H9yzmHyyPmukeccSl2mbN5sDQZYnPg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/auto-instrumentations-node@0.62.1': - resolution: {integrity: sha512-FmPlWS7Dg6E3kP0vv19Pyhq3sqSi8tyn8IZh2RV73UsrcEZeQ3gUTf2Ar8iPRgbsxTukQHRoMGcaCVBsFVRVPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.4.1 - '@opentelemetry/core': ^2.0.0 - - '@opentelemetry/context-async-hooks@2.0.1': - resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + '@opentelemetry/context-async-hooks@2.1.0': + resolution: {integrity: sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.0.1': - resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + '@opentelemetry/core@2.1.0': + resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.203.0': - resolution: {integrity: sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==} + '@opentelemetry/exporter-logs-otlp-grpc@0.205.0': + resolution: {integrity: sha512-jQlw7OHbqZ8zPt+pOrW2KGN7T55P50e3NXBMr4ckPOF+DWDwSy4W7mkG09GpYWlQAQ5C9BXg5gfUlv5ldTgWsw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.203.0': - resolution: {integrity: sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==} + '@opentelemetry/exporter-logs-otlp-http@0.205.0': + resolution: {integrity: sha512-5JteMyVWiro4ghF0tHQjfE6OJcF7UBUcoEqX3UIQ5jutKP1H+fxFdyhqjjpmeHMFxzOHaYuLlNR1Bn7FOjGyJg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.203.0': - resolution: {integrity: sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==} + '@opentelemetry/exporter-logs-otlp-proto@0.205.0': + resolution: {integrity: sha512-q3VS9wS+lpZ01txKxiDGBtBpTNge3YhbVEFDgem9ZQR9eI3EZ68+9tVZH9zJcSxI37nZPJ6lEEZO58yEjYZsVA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0': - resolution: {integrity: sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.205.0': + resolution: {integrity: sha512-1Vxlo4lUwqSKYX+phFkXHKYR3DolFHxCku6lVMP1H8sVE3oj4wwmwxMzDsJ7zF+sXd8M0FCr+ckK4SnNNKkV+w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.203.0': - resolution: {integrity: sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==} + '@opentelemetry/exporter-metrics-otlp-http@0.205.0': + resolution: {integrity: sha512-fFxNQ/HbbpLmh1pgU6HUVbFD1kNIjrkoluoKJkh88+gnmpFD92kMQ8WFNjPnSbjg2mNVnEkeKXgCYEowNW+p1w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.203.0': - resolution: {integrity: sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==} + '@opentelemetry/exporter-metrics-otlp-proto@0.205.0': + resolution: {integrity: sha512-qIbNnedw9QfFjwpx4NQvdgjK3j3R2kWH/2T+7WXAm1IfMFe9fwatYxE61i7li4CIJKf8HgUC3GS8Du0C3D+AuQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.203.0': - resolution: {integrity: sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==} + '@opentelemetry/exporter-prometheus@0.205.0': + resolution: {integrity: sha512-xsot/Qm9VLDTag4GEwAunD1XR1U8eBHTLAgO7IZNo2JuD/c/vL7xmDP7mQIUr6Lk3gtj/yGGIR2h3vhTeVzv4w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.203.0': - resolution: {integrity: sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==} + '@opentelemetry/exporter-trace-otlp-grpc@0.205.0': + resolution: {integrity: sha512-ZBksUk84CcQOuDJB65yu5A4PORkC4qEsskNwCrPZxDLeWjPOFZNSWt0E0jQxKCY8PskLhjNXJYo12YaqsYvGFA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.203.0': - resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} + '@opentelemetry/exporter-trace-otlp-http@0.205.0': + resolution: {integrity: sha512-vr2bwwPCSc9u7rbKc74jR+DXFvyMFQo9o5zs+H/fgbK672Whw/1izUKVf+xfWOdJOvuwTnfWxy+VAY+4TSo74Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.203.0': - resolution: {integrity: sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==} + '@opentelemetry/exporter-trace-otlp-proto@0.205.0': + resolution: {integrity: sha512-bGtFzqiENO2GpJk988mOBMe0MfeNpTQjbLm/LBijas6VRyEDQarUzdBHpFlu89A25k1+BCntdWGsWTa9Ai4FyA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.0.1': - resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + '@opentelemetry/exporter-zipkin@2.1.0': + resolution: {integrity: sha512-0mEI0VDZrrX9t5RE1FhAyGz+jAGt96HSuXu73leswtY3L5YZD11gtcpARY2KAx/s6Z2+rj5Mhj566JsI2C7mfA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3304,284 +3290,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-amqplib@0.50.0': - resolution: {integrity: sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==} + '@opentelemetry/instrumentation-http@0.205.0': + resolution: {integrity: sha512-6fOgRlV7ypBuEzCQP7vXkLQxz3UL1FhE24rAlMRbwGvPAnZLvutcG/fq9FI/n+VU23dOpYexocYsXCf5oy/AXw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-aws-lambda@0.54.0': - resolution: {integrity: sha512-uiYI+kcMUJ/H9cxAwB8c9CaG8behLRgcYSOEA8M/tMQ54Y1ZmzAuEE3QKOi21/s30x5Q+by9g7BwiVfDtqzeMA==} + '@opentelemetry/instrumentation-ioredis@0.53.0': + resolution: {integrity: sha512-Ah2wU347vOJYbE563Tgm3UX2J3DAXoI8gsr8qH0OOO4uDuEv3kVS/eDCfXApt11bvvDDPlOoc60/TGn6m9IoPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-aws-sdk@0.57.0': - resolution: {integrity: sha512-RfbyjaeZzX3mPhuaRHlSAQyfX3skfeWOl30jrqSXtE9k0DPdnIqpHhdYS0C/DEDuZbwTmruVJ4cUwMBw5Z6FAg==} + '@opentelemetry/instrumentation-nestjs-core@0.51.0': + resolution: {integrity: sha512-Se/m4887W94OO12pjKMjI3398L7HCoWeCjcbwoPvNOWpSpMkljBOHA9vE/fyo63CaVG1XAM5xA4ad60wmJKl9A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-bunyan@0.49.0': - resolution: {integrity: sha512-ky5Am1y6s3Ex/3RygHxB/ZXNG07zPfg9Z6Ora+vfeKcr/+I6CJbWXWhSBJor3gFgKN3RvC11UWVURnmDpBS6Pg==} + '@opentelemetry/instrumentation-pg@0.58.0': + resolution: {integrity: sha512-WHntZAorf6CZ0n5a3oHlwGkSeu5Xa4AiCmXkNTKg24TbYSFWzJUtWvPQSkxePvQ3ku71lhAY/M20WgwHlvpZpQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-cassandra-driver@0.49.0': - resolution: {integrity: sha512-BNIvqldmLkeikfI5w5Rlm9vG5NnQexfPoxOgEMzfDVOEF+vS6351I6DzWLLgWWR9CNF/jQJJi/lr6am2DLp0Rw==} + '@opentelemetry/instrumentation@0.205.0': + resolution: {integrity: sha512-cgvm7tvQdu9Qo7VurJP84wJ7ZV9F6WqDDGZpUc6rUEXwjV7/bXWs0kaYp9v+1Vh1+3TZCD3i6j/lUBcPhu8NhA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-connect@0.47.0': - resolution: {integrity: sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==} + '@opentelemetry/otlp-exporter-base@0.205.0': + resolution: {integrity: sha512-2MN0C1IiKyo34M6NZzD6P9Nv9Dfuz3OJ3rkZwzFmF6xzjDfqqCTatc9v1EpNfaP55iDOCLHFyYNCgs61FFgtUQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-cucumber@0.18.1': - resolution: {integrity: sha512-gTfT7AuA0UH0TvqWOXnyr2KCv7mvZsOUmqCrtnU/RDcZ9J3nIX4OBfl7VVXE0fJlLqP7KIDggQ8O9g7rmaVLhA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/instrumentation-dataloader@0.21.1': - resolution: {integrity: sha512-hNAm/bwGawLM8VDjKR0ZUDJ/D/qKR3s6lA5NV+btNaPVm2acqhPcT47l2uCVi+70lng2mywfQncor9v8/ykuyw==} + '@opentelemetry/otlp-grpc-exporter-base@0.205.0': + resolution: {integrity: sha512-AeuLfrciGYffqsp4EUTdYYc6Ee2BQS+hr08mHZk1C524SFWx0WnfcTnV0NFXbVURUNU6DZu1DhS89zRRrcx/hg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-dns@0.47.0': - resolution: {integrity: sha512-775fOnewWkTF4iXMGKgwvOGqEmPrU1PZpXjjqvTrEErYBJe7Fz1WlEeUStHepyKOdld7Ghv7TOF/kE3QDctvrg==} + '@opentelemetry/otlp-transformer@0.205.0': + resolution: {integrity: sha512-KmObgqPtk9k/XTlWPJHdMbGCylRAmMJNXIRh6VYJmvlRDMfe+DonH41G7eenG8t4FXn3fxOGh14o/WiMRR6vPg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-express@0.52.0': - resolution: {integrity: sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fastify@0.48.0': - resolution: {integrity: sha512-3zQlE/DoVfVH6/ycuTv7vtR/xib6WOa0aLFfslYcvE62z0htRu/ot8PV/zmMZfnzpTQj8S/4ULv36R6UIbpJIg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fs@0.23.0': - resolution: {integrity: sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-generic-pool@0.47.0': - resolution: {integrity: sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-graphql@0.51.0': - resolution: {integrity: sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-grpc@0.203.0': - resolution: {integrity: sha512-Qmjx2iwccHYRLoE4RFS46CvQE9JG9Pfeae4EPaNZjvIuJxb/pZa2R9VWzRlTehqQWpAvto/dGhtkw8Tv+o0LTg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-hapi@0.50.0': - resolution: {integrity: sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-http@0.203.0': - resolution: {integrity: sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-ioredis@0.51.0': - resolution: {integrity: sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-kafkajs@0.13.0': - resolution: {integrity: sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-knex@0.48.0': - resolution: {integrity: sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-koa@0.51.0': - resolution: {integrity: sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-lru-memoizer@0.48.0': - resolution: {integrity: sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-memcached@0.47.0': - resolution: {integrity: sha512-vXDs/l4hlWy1IepPG1S6aYiIZn+tZDI24kAzwKKJmR2QEJRL84PojmALAEJGazIOLl/VdcCPZdMb0U2K0VzojA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongodb@0.56.0': - resolution: {integrity: sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongoose@0.50.0': - resolution: {integrity: sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql2@0.50.0': - resolution: {integrity: sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql@0.49.0': - resolution: {integrity: sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-nestjs-core@0.49.0': - resolution: {integrity: sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-net@0.47.0': - resolution: {integrity: sha512-csoJ++Njpf7C09JH+0HNGenuNbDZBqO1rFhMRo6s0rAmJwNh9zY3M/urzptmKlqbKnf4eH0s+CKHy/+M8fbFsQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-oracledb@0.29.0': - resolution: {integrity: sha512-2aHLiJdkyiUbooIUm7FaZf+O4jyqEl+RfFpgud1dxT87QeeYM216wi+xaMNzsb5yKtRBqbA3qeHBCyenYrOZwA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-pg@0.56.0': - resolution: {integrity: sha512-A/J4SlGX8Y0Wwp7Y66fsNCFT/1h9lmBzqwTnfWW/bULtcKFqkQfqhs3G8+4cRxX02UI2z7T1aW5bsyc6QSYc1Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-pino@0.50.0': - resolution: {integrity: sha512-Pi0cWGp4f2gresq2xqef4IsuunLdebJ9n9tZxytDz2ci4euIfW36ILpszQmRNhwCVDCZLmUgGDKZGj4PXyPd0w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-redis@0.51.0': - resolution: {integrity: sha512-uL/GtBA0u72YPPehwOvthAe+Wf8k3T+XQPBssJmTYl6fzuZjNq8zTfxVFhl9nRFjFVEe+CtiYNT0Q3AyqW1Z0A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-restify@0.49.0': - resolution: {integrity: sha512-tsGZZhS4mVZH7omYxw5jpsrD3LhWizqWc0PYtAnzpFUvL5ZINHE+cm57bssTQ2AK/GtZMxu9LktwCvIIf3dSmw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-router@0.48.0': - resolution: {integrity: sha512-Wixrc8CchuJojXpaS/dCQjFOMc+3OEil1H21G+WLYQb8PcKt5kzW9zDBT19nyjjQOx/D/uHPfgbrT+Dc7cfJ9w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-runtime-node@0.17.1': - resolution: {integrity: sha512-c1FlAk+bB2uF9a8YneGmNPTl7c/xVaan4mmWvbkWcOmH/ipKqR1LaKUlz/BMzLrJLjho1EJlG2NrS2w2Arg+nw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-socket.io@0.50.0': - resolution: {integrity: sha512-6JN6lnKN9ZuZtZdMQIR+no1qHzQvXSZUsNe3sSWMgqmNRyEXuDUWBIyKKeG0oHRHtR4xE4QhJyD4D5kKRPWZFA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-tedious@0.22.0': - resolution: {integrity: sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-undici@0.14.0': - resolution: {integrity: sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.7.0 - - '@opentelemetry/instrumentation-winston@0.48.1': - resolution: {integrity: sha512-XyOuVwdziirHHYlsw+BWrvdI/ymjwnexupKA787zQQ+D5upaE/tseZxjfQa7+t4+FdVLxHICaMTmkSD4yZHpzQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation@0.203.0': - resolution: {integrity: sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/otlp-exporter-base@0.203.0': - resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/otlp-grpc-exporter-base@0.203.0': - resolution: {integrity: sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/otlp-transformer@0.203.0': - resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/propagation-utils@0.31.3': - resolution: {integrity: sha512-ZI6LKjyo+QYYZY5SO8vfoCQ9A69r1/g+pyjvtu5RSK38npINN1evEmwqbqhbg2CdcIK3a4PN6pDAJz/yC5/gAA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/propagator-b3@2.0.1': - resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + '@opentelemetry/propagator-b3@2.1.0': + resolution: {integrity: sha512-yOdHmFseIChYanddMMz0mJIFQHyjwbNhoxc65fEAA8yanxcBPwoFDoh1+WBUWAO/Z0NRgk+k87d+aFIzAZhcBw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.0.1': - resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + '@opentelemetry/propagator-jaeger@2.1.0': + resolution: {integrity: sha512-QYo7vLyMjrBCUTpwQBF/e+rvP7oGskrSELGxhSvLj5gpM0az9oJnu/0O4l2Nm7LEhAff80ntRYKkAcSwVgvSVQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3590,74 +3354,44 @@ packages: resolution: {integrity: sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resource-detector-alibaba-cloud@0.31.3': - resolution: {integrity: sha512-I556LHcLVsBXEgnbPgQISP/JezDt5OfpgOaJNR1iVJl202r+K145OSSOxnH5YOc/KvrydBD0FOE03F7x0xnVTw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-aws@2.3.0': - resolution: {integrity: sha512-PkD/lyXG3B3REq1Y6imBLckljkJYXavtqGYSryAeJYvGOf5Ds3doR+BCGjmKeF6ObAtI5MtpBeUStTDtGtBsWA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-azure@0.10.0': - resolution: {integrity: sha512-5cNAiyPBg53Uxe/CW7hsCq8HiKNAUGH+gi65TtgpzSR9bhJG4AEbuZhbJDFwe97tn2ifAD1JTkbc/OFuaaFWbA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-container@0.7.3': - resolution: {integrity: sha512-SK+xUFw6DKYbQniaGmIFsFxAZsr8RpRSRWxKi5/ZJAoqqPnjcyGI/SeUx8zzPk4XLO084zyM4pRHgir0hRTaSQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-gcp@0.37.0': - resolution: {integrity: sha512-LGpJBECIMsVKhiulb4nxUw++m1oF4EiDDPmFGW2aqYaAF0oUvJNv8Z/55CAzcZ7SxvlTgUwzewXDBsuCup7iqw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resources@2.0.1': - resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + '@opentelemetry/resources@2.1.0': + resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.203.0': - resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} + '@opentelemetry/sdk-logs@0.205.0': + resolution: {integrity: sha512-nyqhNQ6eEzPWQU60Nc7+A5LIq8fz3UeIzdEVBQYefB4+msJZ2vuVtRuk9KxPMw1uHoHDtYEwkr2Ct0iG29jU8w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.0.1': - resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + '@opentelemetry/sdk-metrics@2.1.0': + resolution: {integrity: sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.203.0': - resolution: {integrity: sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==} + '@opentelemetry/sdk-node@0.205.0': + resolution: {integrity: sha512-Y4Wcs8scj/Wy1u61pX1ggqPXPtCsGaqx/UnFu7BtRQE1zCQR+b0h56K7I0jz7U2bRlPUZIFdnNLtoaJSMNzz2g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.0.1': - resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + '@opentelemetry/sdk-trace-base@2.1.0': + resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.0.1': - resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + '@opentelemetry/sdk-trace-node@2.1.0': + resolution: {integrity: sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.36.0': - resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} + '@opentelemetry/semantic-conventions@1.37.0': + resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} engines: {node: '>=14'} '@opentelemetry/sql-common@0.41.0': @@ -3669,30 +3403,30 @@ packages: '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} - '@photo-sphere-viewer/core@5.13.4': - resolution: {integrity: sha512-leVQL6gG9wTF+uvCFarHUcr8mzafCZ/GLzauksYQJfiqDVRFSAJNXnTOy7RH9otToluEdjN1hsN1f9HQy+rLYg==} + '@photo-sphere-viewer/core@5.14.0': + resolution: {integrity: sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==} - '@photo-sphere-viewer/equirectangular-video-adapter@5.13.4': - resolution: {integrity: sha512-OdTOKxFunP56FNoPR47mQp7V1WHvV4eiow3qtyJjAgLeU8T2q3kivLuH1kMZN2yTAJaXab+VBXzA/YChiHZ6mQ==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.14.0': + resolution: {integrity: sha512-Ez88sZ4sj3fONpZSortnN3gLXlvV/hn5U/88LsWtxI73YwhkZ06ZtXFYLXU4MBaJvqCbMGaR6j39uVXTWFo5rw==} peerDependencies: - '@photo-sphere-viewer/core': 5.13.4 - '@photo-sphere-viewer/video-plugin': 5.13.4 + '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/video-plugin': 5.14.0 - '@photo-sphere-viewer/resolution-plugin@5.13.4': - resolution: {integrity: sha512-HRBC5zYmpNoo/joKZzXbxn7jwoh3tdtTJFXzHxYPV51ELDclRNmzhmqEaZeVkrFHr4bRF5ow3AOjxiMtu1xQxA==} + '@photo-sphere-viewer/resolution-plugin@5.14.0': + resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==} peerDependencies: - '@photo-sphere-viewer/core': 5.13.4 - '@photo-sphere-viewer/settings-plugin': 5.13.4 + '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/settings-plugin': 5.14.0 - '@photo-sphere-viewer/settings-plugin@5.13.4': - resolution: {integrity: sha512-As1nmlsfnjKBFQOWPVQLH1+dJ+s62MdEb6Jvlm16+3fUVHF4CBWRTJZyBKejLiu4xjbDxrE8v5ZHDLvG6ButiQ==} + '@photo-sphere-viewer/settings-plugin@5.14.0': + resolution: {integrity: sha512-sMLX4hFSE2PjiP2iUmH9qUAz6GV+UN2WX1zu/D58BBWzF3+8mV+FC9l50qxruO8qvWqqLwYysHUElHnmPPtpTg==} peerDependencies: - '@photo-sphere-viewer/core': 5.13.4 + '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/video-plugin@5.13.4': - resolution: {integrity: sha512-QWbHMVAJHukLbFNn0irND/nEPtmzjbXth1ckBkT1bg8aRilFw50+IIB0Zfdl6X919R2GfGo8P0u+I/Mwxf7yfg==} + '@photo-sphere-viewer/video-plugin@5.14.0': + resolution: {integrity: sha512-jWMZBNlfwYq8Lgc8ncs3ptwHR6Yk7Wl8o1BCFYhmhoRkGZFHEjoOQj7gMPXCET+3iYXQ1TsjTh4ZCW8UUOi+pg==} peerDependencies: - '@photo-sphere-viewer/core': 5.13.4 + '@photo-sphere-viewer/core': 5.14.0 '@photostructure/tz-lookup@11.2.0': resolution: {integrity: sha512-DwrvodcXHNSdGdeSF7SBL5o8aBlsaeuCuG7633F04nYsL3hn5Hxe3z/5kCqxv61J1q7ggKZ27GPylR3x0cPNXQ==} @@ -3705,8 +3439,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.54.2': - resolution: {integrity: sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==} + '@playwright/test@1.55.0': + resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==} engines: {node: '>=18'} hasBin: true @@ -3784,8 +3518,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/components@0.5.0': - resolution: {integrity: sha512-esRbP+yMmSkNP9hcpiy2RwpDnvSmlxJcJ1HHbzSwlACGlCHTap+ma344QovvzhpVRhMccyWemdClLG822UvVpQ==} + '@react-email/components@0.5.3': + resolution: {integrity: sha512-8G5vsoMehuGOT4cDqaYLdpagtqCYPl4vThXNylClxO6SrN2w9Mh1+i2RNGj/rdqh/woamHORjlXMYCA/kzDMew==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -3849,8 +3583,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/render@1.2.0': - resolution: {integrity: sha512-5fpbV16VYR9Fmk8t7xiwPNAjxjdI8XzVtlx9J9OkhOsIHdr2s5DwAj8/MXzWa9qRYJyLirQ/l7rBSjjgyRAomw==} + '@react-email/render@1.2.3': + resolution: {integrity: sha512-qu3XYNkHGao3teJexVD5CrcgFkNLrzbZvpZN17a7EyQYUN3kHkTkE9saqY4VbvGx6QoNU3p8rsk/Xm++D/+pTw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -3880,19 +3614,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@reduxjs/toolkit@1.9.7': - resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - - '@rollup/pluginutils@5.2.0': - resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -3900,103 +3623,108 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.46.3': - resolution: {integrity: sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==} + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.3': - resolution: {integrity: sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==} + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.3': - resolution: {integrity: sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==} + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.3': - resolution: {integrity: sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==} + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.3': - resolution: {integrity: sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==} + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.3': - resolution: {integrity: sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==} + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.3': - resolution: {integrity: sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.46.3': - resolution: {integrity: sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==} + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.46.3': - resolution: {integrity: sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==} + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.46.3': - resolution: {integrity: sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==} + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.46.3': - resolution: {integrity: sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==} + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.46.3': - resolution: {integrity: sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==} + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.46.3': - resolution: {integrity: sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==} + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.46.3': - resolution: {integrity: sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==} + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.46.3': - resolution: {integrity: sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==} + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.46.3': - resolution: {integrity: sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==} + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.46.3': - resolution: {integrity: sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==} + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.46.3': - resolution: {integrity: sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==} + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.3': - resolution: {integrity: sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==} + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.3': - resolution: {integrity: sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==} + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} cpu: [x64] os: [win32] @@ -4035,6 +3763,174 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + '@smithy/abort-controller@4.1.1': + resolution: {integrity: sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.2.2': + resolution: {integrity: sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.11.0': + resolution: {integrity: sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.1.2': + resolution: {integrity: sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.2.1': + resolution: {integrity: sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.1.1': + resolution: {integrity: sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.1.1': + resolution: {integrity: sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.1.0': + resolution: {integrity: sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.1.1': + resolution: {integrity: sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.2.2': + resolution: {integrity: sha512-M51KcwD+UeSOFtpALGf5OijWt915aQT5eJhqnMKJt7ZTfDfNcvg2UZgIgTZUoiORawb6o5lk4n3rv7vnzQXgsA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.2.2': + resolution: {integrity: sha512-KZJueEOO+PWqflv2oGx9jICpHdBYXwCI19j7e2V3IMwKgFcXc9D9q/dsTf4B+uCnYxjNoS1jpyv6pGNGRsKOXA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.1.1': + resolution: {integrity: sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.1.1': + resolution: {integrity: sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.2.2': + resolution: {integrity: sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.2.1': + resolution: {integrity: sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.1.1': + resolution: {integrity: sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.2.1': + resolution: {integrity: sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.1.1': + resolution: {integrity: sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.1.1': + resolution: {integrity: sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.1.1': + resolution: {integrity: sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.2.0': + resolution: {integrity: sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.2.1': + resolution: {integrity: sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.6.2': + resolution: {integrity: sha512-u82cjh/x7MlMat76Z38TRmEcG6JtrrxN4N2CSNG5o2v2S3hfLAxRgSgFqf0FKM3dglH41Evknt/HOX+7nfzZ3g==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.5.0': + resolution: {integrity: sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.1.1': + resolution: {integrity: sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.1.0': + resolution: {integrity: sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.1.0': + resolution: {integrity: sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.1.0': + resolution: {integrity: sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.1.0': + resolution: {integrity: sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.1.0': + resolution: {integrity: sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.1.2': + resolution: {integrity: sha512-QKrOw01DvNHKgY+3p4r9Ut4u6EHLVZ01u6SkOMe6V6v5C+nRPXJeWh72qCT1HgwU3O7sxAIu23nNh+FOpYVZKA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.1.2': + resolution: {integrity: sha512-l2yRmSfx5haYHswPxMmCR6jGwgPs5LjHLuBwlj9U7nNBMS43YV/eevj+Xq1869UYdiynnMrCKtoOYQcwtb6lKg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.1.2': + resolution: {integrity: sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.1.0': + resolution: {integrity: sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.1.1': + resolution: {integrity: sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.1.1': + resolution: {integrity: sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.3.1': + resolution: {integrity: sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.1.0': + resolution: {integrity: sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.1.0': + resolution: {integrity: sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==} + engines: {node: '>=18.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -4044,9 +3940,6 @@ packages: peerDependencies: socket.io-adapter: ^2.5.4 - '@sqltools/formatter@1.2.5': - resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} - '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -4067,14 +3960,18 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.27.1': - resolution: {integrity: sha512-u5HbL9T4TgWZwXZM7hwdT0f5sDkGaNxsSrLYQoql+eiz2+9rcbbq4MiOAPoRtXG0dys5P5ixBmyQdqZedwZUlA==} + '@sveltejs/kit@2.38.1': + resolution: {integrity: sha512-5JJBPu3U2KXpRwc+e/D2Pl+DJM9oBcCl6XtWenrb6xc6H4lFa0XIJaSch4wMiADrhX512sVIUf13VnEp7aWO1w==} engines: {node: '>=18.13'} hasBin: true peerDependencies: + '@opentelemetry/api': ^1.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true '@sveltejs/vite-plugin-svelte-inspector@5.0.0': resolution: {integrity: sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==} @@ -4084,8 +3981,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@sveltejs/vite-plugin-svelte@6.1.2': - resolution: {integrity: sha512-7v+7OkUYelC2dhhYDAgX1qO2LcGscZ18Hi5kKzJQq7tQeXpH215dd0+J/HnX2zM5B3QKcIrTVqCGkZXAy5awYw==} + '@sveltejs/vite-plugin-svelte@6.2.0': + resolution: {integrity: sha512-nJsV36+o7rZUDlrnSduMNl11+RoDE1cKqOI0yUEBCcqFoAZOk47TwD3dPKS2WmRutke9StXnzsPBslY7prDM9w==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.0.0 @@ -4169,68 +4066,68 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.13.3': - resolution: {integrity: sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==} + '@swc/core-darwin-arm64@1.13.5': + resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.3': - resolution: {integrity: sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==} + '@swc/core-darwin-x64@1.13.5': + resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.3': - resolution: {integrity: sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==} + '@swc/core-linux-arm-gnueabihf@1.13.5': + resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.3': - resolution: {integrity: sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==} + '@swc/core-linux-arm64-gnu@1.13.5': + resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.13.3': - resolution: {integrity: sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==} + '@swc/core-linux-arm64-musl@1.13.5': + resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.13.3': - resolution: {integrity: sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==} + '@swc/core-linux-x64-gnu@1.13.5': + resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.13.3': - resolution: {integrity: sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==} + '@swc/core-linux-x64-musl@1.13.5': + resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.13.3': - resolution: {integrity: sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==} + '@swc/core-win32-arm64-msvc@1.13.5': + resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.3': - resolution: {integrity: sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==} + '@swc/core-win32-ia32-msvc@1.13.5': + resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.3': - resolution: {integrity: sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==} + '@swc/core-win32-x64-msvc@1.13.5': + resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.3': - resolution: {integrity: sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==} + '@swc/core@1.13.5': + resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4244,72 +4141,72 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@swc/types@0.1.24': - resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tailwindcss/node@4.1.12': - resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} + '@tailwindcss/node@4.1.13': + resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} - '@tailwindcss/oxide-android-arm64@4.1.12': - resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==} + '@tailwindcss/oxide-android-arm64@4.1.13': + resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.12': - resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==} + '@tailwindcss/oxide-darwin-arm64@4.1.13': + resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.12': - resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==} + '@tailwindcss/oxide-darwin-x64@4.1.13': + resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.12': - resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==} + '@tailwindcss/oxide-freebsd-x64@4.1.13': + resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': - resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': - resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.12': - resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.12': - resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.12': - resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.12': - resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4320,39 +4217,33 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': - resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.12': - resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.12': - resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} + '@tailwindcss/oxide@4.1.13': + resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} engines: {node: '>= 10'} - '@tailwindcss/vite@4.1.12': - resolution: {integrity: sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==} + '@tailwindcss/vite@4.1.13': + resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@testcontainers/postgresql@11.5.1': - resolution: {integrity: sha512-6P1QYIKRkktSVwTuwU0Pke5WbXTkvpLleyQcgknJPbZwhaIsCrhnbZlVzj2g/e+Nf9Lmdy1F2OAai+vUrBq0AQ==} - - '@testcontainers/redis@11.5.1': - resolution: {integrity: sha512-ThGaUPUCFW4Vwmx6kfPYhhTQjq/3UXJQrU/xxiYLqgvFJNtvtYlWmzXrwORLhPkkqnoFUnfFaX3u9u1GnrlDkw==} - '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.7.0': - resolution: {integrity: sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==} + '@testing-library/jest-dom@6.8.0': + resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} '@testing-library/svelte@5.2.8': @@ -4410,9 +4301,6 @@ packages: '@types/async-lock@1.4.2': resolution: {integrity: sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==} - '@types/aws-lambda@8.10.150': - resolution: {integrity: sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==} - '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} @@ -4425,9 +4313,6 @@ packages: '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} - '@types/bunyan@1.8.11': - resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} - '@types/byte-size@8.1.2': resolution: {integrity: sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==} @@ -4541,9 +4426,6 @@ packages: '@types/history@4.7.11': resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - '@types/hoist-non-react-statics@3.3.6': - resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} - '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} @@ -4589,15 +4471,12 @@ packages: '@types/koa@3.0.0': resolution: {integrity: sha512-MOcVYdVYmkSutVHZZPh8j3+dAjLyR5Tl59CN0eKgpkE1h/LBSmPAsQQuWs+bKu7WtGNn+hKfJH9Gzml+PulmDg==} - '@types/leaflet@1.9.19': - resolution: {integrity: sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==} + '@types/leaflet@1.9.20': + resolution: {integrity: sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==} '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.19': - resolution: {integrity: sha512-NYqRyg/hIQrYPT9lbOeYc3kIRabJDn/k4qQHIXUpx88CBDww2fD15Sg5kbXlW86zm2XEW4g0QxkTI3/Kfkc7xQ==} - '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} @@ -4613,9 +4492,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/memcached@2.2.10': - resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} - '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -4634,41 +4510,29 @@ packages: '@types/multer@2.0.0': resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} - '@types/mysql@2.15.27': - resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - - '@types/node-fetch@2.6.12': - resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@18.19.123': - resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} + '@types/node@18.19.126': + resolution: {integrity: sha512-8AXQlBfrGmtYJEJUPs63F/uZQqVeFiN9o6NUjbDJYfxNxFnArlZufANPw4h6dGhYGKxcyw+TapXFvEsguzIQow==} '@types/node@20.19.2': resolution: {integrity: sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==} - '@types/node@22.13.14': - resolution: {integrity: sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==} + '@types/node@22.18.5': + resolution: {integrity: sha512-g9BpPfJvxYBXUWI9bV37j6d6LTMNQ88hPwdWWUeYZnMhlo66FIg9gCc1/DZb15QylJSKwOZjwrckvOTWpOiChg==} - '@types/node@22.17.2': - resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + '@types/node@24.5.1': + resolution: {integrity: sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q==} - '@types/node@24.3.0': - resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} + '@types/nodemailer@7.0.1': + resolution: {integrity: sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==} - '@types/nodemailer@6.4.17': - resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} - - '@types/oidc-provider@9.1.2': - resolution: {integrity: sha512-JAreXkbWsZR72Gt3eigG652wq1qBcjhuy421PXU2a8PS0mM00XlG+UdXbM/QPihM3ko0YF8cwvt0H2kacXGcsg==} - - '@types/oracledb@6.5.2': - resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} + '@types/oidc-provider@9.5.0': + resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} @@ -4676,9 +4540,6 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - '@types/pg@8.15.4': - resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==} - '@types/pg@8.15.5': resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} @@ -4700,9 +4561,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-redux@7.1.34': - resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} - '@types/react-router-config@5.0.11': resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} @@ -4712,11 +4570,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.1.10': - resolution: {integrity: sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==} - - '@types/react@19.1.8': - resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + '@types/react@19.1.13': + resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -4730,8 +4585,8 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} '@types/send@0.17.5': resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} @@ -4763,9 +4618,6 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} - '@types/tedious@4.0.14': - resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} @@ -4778,8 +4630,11 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/validator@13.15.2': - resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + + '@types/validator@13.15.3': + resolution: {integrity: sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==} '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -4793,107 +4648,63 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@8.39.1': - resolution: {integrity: sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==} + '@typescript-eslint/eslint-plugin@8.43.0': + resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.39.1 + '@typescript-eslint/parser': ^8.43.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.39.1': - resolution: {integrity: sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==} + '@typescript-eslint/parser@8.43.0': + resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.35.0': - resolution: {integrity: sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/project-service@8.39.1': - resolution: {integrity: sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==} + '@typescript-eslint/project-service@8.43.0': + resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.35.0': - resolution: {integrity: sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==} + '@typescript-eslint/scope-manager@8.43.0': + resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.39.1': - resolution: {integrity: sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.35.0': - resolution: {integrity: sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/tsconfig-utils@8.39.1': - resolution: {integrity: sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==} + '@typescript-eslint/tsconfig-utils@8.43.0': + resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.35.0': - resolution: {integrity: sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/type-utils@8.39.1': - resolution: {integrity: sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==} + '@typescript-eslint/type-utils@8.43.0': + resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.35.0': - resolution: {integrity: sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==} + '@typescript-eslint/types@8.43.0': + resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.39.1': - resolution: {integrity: sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.35.0': - resolution: {integrity: sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/typescript-estree@8.39.1': - resolution: {integrity: sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==} + '@typescript-eslint/typescript-estree@8.43.0': + resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.35.0': - resolution: {integrity: sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/utils@8.39.1': - resolution: {integrity: sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==} + '@typescript-eslint/utils@8.43.0': + resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.35.0': - resolution: {integrity: sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.39.1': - resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} + '@typescript-eslint/visitor-keys@8.43.0': + resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5063,14 +4874,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ajv-draft-04@1.0.0: - resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} - peerDependencies: - ajv: ^8.5.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -5100,9 +4903,6 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.11.0: - resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} - ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -5135,8 +4935,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.0: - resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -5147,14 +4947,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@3.17.0: - resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} - engines: {node: '>=14'} - ansis@4.1.0: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} @@ -5166,10 +4962,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - app-root-path@3.1.0: - resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} - engines: {node: '>= 6.0.0'} - append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -5247,12 +5039,6 @@ packages: async@0.2.10: resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} - async@3.2.2: - resolution: {integrity: sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==} - - async@3.2.4: - resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -5310,9 +5096,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.5.4: - resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} - bare-events@2.6.1: resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} @@ -5370,15 +5153,12 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bits-ui@2.9.4: - resolution: {integrity: sha512-Cqn685P6DDuEyBZT/CWMyS5+8JAnYbctvoEVPcmiut+HUpG3SozVgjoDaUib5VG4ZYUKEi1FPwHxiXo9c6J0PA==} + bits-ui@2.9.8: + resolution: {integrity: sha512-oVAqdhLSuGIgEiT0yu3ShSI7AxncCxX26Gv6Lul94BuKHV2uzHoKfIodtnMQSq+udJ54svuCIRqA58whsv7vaA==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -5401,6 +5181,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + boxen@6.2.1: resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5419,11 +5202,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.25.3: resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -5450,8 +5228,8 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.57.0: - resolution: {integrity: sha512-Xlh5mh6VQmHS6x5PIuYNf55Nn3T7GGN5Is+zHysN4ZUomX3RziyRFzQXeWgn3SKbaXxQ3aLWHjYDMaE5MhEXyA==} + bullmq@5.58.5: + resolution: {integrity: sha512-0A6Qjxdn8j7aOcxfRZY798vO/aMuwvoZwfE6a9EOXHb1pzpBVAogsc/OfRWeUf+5wMBoYB5nthstnJo/zrQOeQ==} busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} @@ -5506,9 +5284,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - call-me-maybe@1.0.2: - resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -5535,9 +5310,6 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001726: - resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} - caniuse-lite@1.0.30001735: resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} @@ -5556,8 +5328,8 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.0: - resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} change-case@5.4.4: @@ -5579,16 +5351,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} - charset@1.0.1: - resolution: {integrity: sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==} - engines: {node: '>=4.0.0'} - check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -5643,9 +5408,6 @@ packages: class-validator@0.14.2: resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} - classnames@2.5.1: - resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} - clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -5709,10 +5471,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -5819,12 +5577,6 @@ packages: resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} - compute-gcd@1.2.1: - resolution: {integrity: sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==} - - compute-lcm@1.1.2: - resolution: {integrity: sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -5916,9 +5668,6 @@ packages: peerDependencies: webpack: ^5.1.0 - core-js-compat@3.43.0: - resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==} - core-js-compat@3.45.0: resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==} @@ -5969,9 +5718,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - crypto-random-string@4.0.0: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} @@ -6137,9 +5883,6 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -6164,8 +5907,8 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -6177,9 +5920,6 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} - decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -6194,14 +5934,6 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -6273,24 +6005,20 @@ packages: detect-europe-js@0.1.2: resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.0: + resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} engines: {node: '>=8'} detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - detect-package-manager@3.0.2: - resolution: {integrity: sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==} - engines: {node: '>=12'} - detect-port@1.6.1: resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} engines: {node: '>= 4.0.0'} hasBin: true - devalue@5.1.1: - resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + devalue@5.3.2: + resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -6345,31 +6073,6 @@ packages: react: ^16.8.4 || ^17 || ^18 || ^19 react-dom: ^16.8.4 || ^17 || ^18 || ^19 - docusaurus-plugin-openapi@0.7.6: - resolution: {integrity: sha512-LR8DI0gO9WFy8K+r0xrVgqDkKKA9zQtDgOnX9CatP3I3Oz5lKegfTJM2fVUIp5m25elzHL+vVKNHS12Jg7sWVA==} - engines: {node: '>=18'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - - docusaurus-plugin-proxy@0.7.6: - resolution: {integrity: sha512-MgjzMEsQOHMljwQGglXXoGjQvs0v1DklhRgzqNLKFwpHB9xLWJZ0KQ3GgbPerW/2vy8tWGJeVhKHy5cPrmweUw==} - engines: {node: '>=14'} - - docusaurus-preset-openapi@0.7.6: - resolution: {integrity: sha512-QnArH/3X0lePB7667FyNK3EeTS8ZP8V2PQxz5m+3BMO2kIzdXDwfTIQ37boB0BTqsDfUE0yCWTVjB0W/BA1UXA==} - engines: {node: '>=18'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - - docusaurus-theme-openapi@0.7.6: - resolution: {integrity: sha512-euoEh8tYX/ssQcMQxBOxt3wPttz3zvPu0l5lSe6exiIwMrORB4O2b8XRB7fVa/awF7xzdIkKHMH55uc5zVOKYA==} - engines: {node: '>=18'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -6417,12 +6120,8 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dotenv@17.2.1: - resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -6444,14 +6143,11 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.177: - resolution: {integrity: sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==} - electron-to-chromium@1.5.207: resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==} - emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -6494,10 +6190,6 @@ packages: resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.18.2: - resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} - engines: {node: '>=10.13.0'} - enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -6520,8 +6212,8 @@ packages: err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} @@ -6549,9 +6241,6 @@ packages: es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - es6-promise@3.3.1: - resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - es6-symbol@3.1.4: resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} engines: {node: '>=0.12'} @@ -6609,9 +6298,10 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-p@0.25.0: - resolution: {integrity: sha512-e7oYgXN/tgtoaR3tZ0R2dKyPJtf5J41hYKsgpsBtwpi0t2Cxjf3l8G2QwrXCDwQTFVXW1hmD55hAqQZxiId1XA==} + eslint-p@0.26.0: + resolution: {integrity: sha512-Y5bDWKIFEUE7dZrbBbq5SiHWadYC4h3+Q+xBAUNNAqU1VMokleoGGfK92Qsmi+EBOLUBbxrtOCND5BSqQn8NaQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: ESLint has built-in support for multithread linting now. This package is no longer needed. hasBin: true eslint-plugin-compat@6.0.2: @@ -6634,8 +6324,8 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-svelte@3.11.0: - resolution: {integrity: sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==} + eslint-plugin-svelte@3.12.3: + resolution: {integrity: sha512-YVNhKsHZeXVvsjZcSMjnce9gO31frICu453p5JjFiXNszHoG9k8WvsA/LAoLi4K8T69G7DIrgg1AqasDJLpgoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -6666,18 +6356,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.30.1: - resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - eslint@9.33.0: - resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==} + eslint@9.35.0: + resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -6775,9 +6455,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter2@6.4.9: - resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -6828,10 +6505,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - fabric@6.7.1: resolution: {integrity: sha512-dLxSmIvN4InJf4xOjbl1LFWh8WGOUIYtcuDIGs2IN0Z9lI0zGobfesDauyEhI1+owMLTPCCiEv01rpYXm7g2EQ==} engines: {node: '>=16.20.0'} @@ -6868,6 +6541,10 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -6915,10 +6592,6 @@ packages: resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} engines: {node: '>=20'} - file-type@3.9.0: - resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} - engines: {node: '>=0.10.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -6976,9 +6649,6 @@ packages: debug: optional: true - foreach@2.0.6: - resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -6994,10 +6664,6 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} - engines: {node: '>= 6'} - form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} @@ -7006,9 +6672,6 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - formidable@2.1.5: - resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} - formidable@3.5.4: resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} engines: {node: '>=14.0.0'} @@ -7074,14 +6737,6 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -7111,8 +6766,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} get-intrinsic@1.3.0: @@ -7130,10 +6785,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stdin@5.0.1: - resolution: {integrity: sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==} - engines: {node: '>=0.12.0'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -7141,9 +6792,6 @@ packages: github-slugger@1.5.0: resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} - gl-matrix@3.4.3: - resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} - gl-matrix@3.4.4: resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} @@ -7187,8 +6835,8 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@16.3.0: - resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} globalyzer@0.1.0: @@ -7205,10 +6853,6 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -7226,9 +6870,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphlib@2.1.8: - resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} - gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -7297,9 +6938,6 @@ packages: hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - hast-util-parse-selector@3.1.1: - resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -7333,9 +6971,6 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - hastscript@7.2.0: - resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} - hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} @@ -7455,12 +7090,6 @@ packages: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} - http-reasons@0.1.0: - resolution: {integrity: sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==} - - http2-client@1.3.5: - resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==} - http2-wrapper@2.2.1: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} @@ -7489,6 +7118,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -7518,9 +7151,6 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} - immer@9.0.21: - resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -7568,18 +7198,14 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + inquirer@8.2.7: + resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} - intl-messageformat@10.7.16: resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} @@ -7833,8 +7459,8 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.0.12: - resolution: {integrity: sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==} + jose@6.1.0: + resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7878,34 +7504,12 @@ packages: engines: {node: '>=6'} hasBin: true - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-pointer@0.6.2: - resolution: {integrity: sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==} - - json-refs@3.0.15: - resolution: {integrity: sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw==} - engines: {node: '>=0.8'} - hasBin: true - - json-schema-compare@0.2.2: - resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==} - - json-schema-merge-allof@0.8.1: - resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} - engines: {node: '>=12.0.0'} - - json-schema-resolve-allof@1.5.0: - resolution: {integrity: sha512-Jgn6BQGSLDp3D7bTYrmCbP/p7SRFz5BfpeEJ9A7sXuVADMc14aaDN1a49zqk9D26wwJlcNvjRpT63cz1VgFZeg==} - hasBin: true - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7926,9 +7530,6 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -8083,10 +7684,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - liquid-json@0.3.1: - resolution: {integrity: sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==} - engines: {node: '>=4'} - load-esm@1.0.2: resolution: {integrity: sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==} engines: {node: '>=13.2.0'} @@ -8180,8 +7777,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.1.0: - resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + lru-cache@11.2.1: + resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -8200,8 +7797,8 @@ packages: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} - luxon@3.7.1: - resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} lz-string@1.5.0: @@ -8211,6 +7808,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -8230,8 +7830,8 @@ packages: resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} engines: {node: '>=6.4.0'} - maplibre-gl@5.6.2: - resolution: {integrity: sha512-SEqYThhUCFf6Lm0TckpgpKnto5u4JsdPYdFJb6g12VtuaFsm3nYdBO+fOmnUYddc8dXihgoGnuXvPPooUcRv5w==} + maplibre-gl@5.7.1: + resolution: {integrity: sha512-iCOQB6W/EGgQx8aU4SyfU5a5/GR2E+ELF92NMsqYfs3x+vnY+8mARmz4gor6XZHCz3tv19mnotVDRlRTMNKyGw==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} mark.js@8.11.1: @@ -8247,11 +7847,6 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - marked@11.2.0: - resolution: {integrity: sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==} - engines: {node: '>= 18'} - hasBin: true - marked@7.0.4: resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} engines: {node: '>= 16'} @@ -8499,9 +8094,6 @@ packages: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-format@2.0.1: - resolution: {integrity: sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==} - mime-types@2.1.18: resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} engines: {node: '>= 0.6'} @@ -8646,9 +8238,6 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - monaco-editor@0.31.1: - resolution: {integrity: sha512-FYPwxGZAeP6mRRyrr5XTGHD9gRXVjy7GUzF4IPChnyt3fS5WrNxIkS8DNujWf6EQy0Zlzpxw8oTVE+mWI2/D1Q==} - moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -8694,9 +8283,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.22.2: - resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} - nan@2.23.0: resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} @@ -8710,9 +8296,6 @@ packages: engines: {node: ^18 || >=20} hasBin: true - native-promise-only@0.8.1: - resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -8735,12 +8318,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - neotraverse@0.6.15: - resolution: {integrity: sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==} - engines: {node: '>= 10'} - - nest-commander@3.18.0: - resolution: {integrity: sha512-NWtodOl2aStnApWp9oajCoJW71lqN0CCjf9ygOWxpXnG3o4nQ8ZO5CgrExfVw2+0CVC877hr0rFR7FSu2rypGg==} + nest-commander@3.19.1: + resolution: {integrity: sha512-Pn6xcMeSnidlzZozNLnbe7P4TqXL7g0JuxqTAtJ89KT4S63ntJZKtRU6g/56h/aHUQa+m98j/c9OxBSduK7EPg==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 @@ -8782,10 +8361,6 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} - node-addon-api@8.4.0: - resolution: {integrity: sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==} - engines: {node: ^18 || ^20 || >= 21} - node-addon-api@8.5.0: resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} @@ -8797,10 +8372,6 @@ packages: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} - node-fetch-h2@2.3.0: - resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} - engines: {node: 4.x || >=6.0.0} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -8822,24 +8393,16 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-gyp@11.2.0: - resolution: {integrity: sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==} + node-gyp@11.4.2: + resolution: {integrity: sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==} engines: {node: ^18.17.0 || >=20.5.0} hasBin: true - node-gyp@11.3.0: - resolution: {integrity: sha512-9J0+C+2nt3WFuui/mC46z2XCZ21/cKlFDuywULmseD/LlmnOrSeEAE4c/1jw6aybXLmpZnQY3/LmOJfgyHIcng==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - - node-readfiles@0.2.0: - resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==} - node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - nodemailer@7.0.5: - resolution: {integrity: sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==} + nodemailer@7.0.6: + resolution: {integrity: sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==} engines: {node: '>=6.0.0'} nopt@1.0.10: @@ -8894,36 +8457,16 @@ packages: peerDependencies: webpack: ^4.0.0 || ^5.0.0 - nwsapi@2.2.21: - resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} + nwsapi@2.2.22: + resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} nypm@0.6.0: resolution: {integrity: sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==} engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oas-kit-common@1.0.8: - resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==} - - oas-linter@3.2.2: - resolution: {integrity: sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==} - - oas-resolver-browser@2.5.6: - resolution: {integrity: sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==} - hasBin: true - - oas-resolver@2.5.6: - resolution: {integrity: sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==} - hasBin: true - - oas-schema-walker@1.1.5: - resolution: {integrity: sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==} - - oas-validator@5.0.8: - resolution: {integrity: sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==} - - oauth4webapi@3.7.0: - resolution: {integrity: sha512-Q52wTPUWPsVLVVmTViXPQFMW2h2xv2jnDGxypjpelCFKaOjLsm7AxYuOk1oQgFm95VNDbuggasu9htXrz6XwKw==} + oauth4webapi@3.8.1: + resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -8951,8 +8494,8 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - oidc-provider@9.4.1: - resolution: {integrity: sha512-luNQK3MBTN6oRliEm+sWVzne8UR+e+Zo0qCWzsY7mhdUNOcjjoe5joFgJrW4i/6mEMYdeWUAPiTGrvggCsyMgQ==} + oidc-provider@9.5.1: + resolution: {integrity: sha512-19Wa4bfz3reoudxrY7sF5SeQKxe5b3dY8hWzQdnBGS87rH0BoYoDDUDRTYciJMN3oI6S02C9xM6vuaHtoZ48eA==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -8977,17 +8520,12 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openapi-to-postmanv2@4.25.0: - resolution: {integrity: sha512-sIymbkQby0gzxt2Yez8YKB6hoISEel05XwGwNrAhr6+vxJWXNxkmssQc/8UEtVkuJ9ZfUXLkip9PYACIpfPDWg==} - engines: {node: '>=8'} - hasBin: true - opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - openid-client@6.6.4: - resolution: {integrity: sha512-PLWVhRksRnNH05sqeuCX/PR+1J70NyZcAcPske+FeF732KKONd3v0p5Utx1ro1iLfCglH8B3/+dA1vqIHDoIiA==} + openid-client@6.8.0: + resolution: {integrity: sha512-oG1d1nAVhIIE+JSjLS+7E9wY1QOJpZltkzlJdbZ7kEn7Hp3hqur2TEeQ8gLOHoHkhbRAGZJKoOnEQcLOQJuIyg==} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -9001,10 +8539,6 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - p-cancelable@3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} @@ -9107,9 +8641,6 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -9129,9 +8660,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-loader@1.0.12: - resolution: {integrity: sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -9159,13 +8687,13 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path@0.12.7: - resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -9245,16 +8773,16 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} - pkg-types@2.2.0: - resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.54.2: - resolution: {integrity: sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==} + playwright-core@1.55.0: + resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==} engines: {node: '>=18'} hasBin: true - playwright@1.54.2: - resolution: {integrity: sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==} + playwright@1.55.0: + resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==} engines: {node: '>=18'} hasBin: true @@ -9422,8 +8950,8 @@ packages: peerDependencies: postcss: ^8.0.0 - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 @@ -9744,18 +9272,6 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} - postman-code-generators@1.14.2: - resolution: {integrity: sha512-qZAyyowfQAFE4MSCu2KtMGGQE/+oG1JhMZMJNMdZHYCSfQiVVeKxgk3oI4+KJ3d1y5rrm2D6C6x+Z+7iyqm+fA==} - engines: {node: '>=12'} - - postman-collection@4.5.0: - resolution: {integrity: sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==} - engines: {node: '>=10'} - - postman-url-encoder@3.0.5: - resolution: {integrity: sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==} - engines: {node: '>=10'} - potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} @@ -9909,8 +9425,8 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - quick-lru@7.0.1: - resolution: {integrity: sha512-kLjThirJMkWKutUKbZ8ViqFc09tDQhlbQo2MNuVeLWbRauqYP96Sm6nzlQ24F0HFjUNZ4i9+AgldJ9H6DZXi7g==} + quick-lru@7.2.0: + resolution: {integrity: sha512-fG4L8TlD1CacJiGMGPxM1/K8l/GaKL2eFQZ6DWAjxZYxSf07DkumbC/Mhh+u/NHvxkfQVL25By0pxBS8QE9ZrQ==} engines: {node: '>=18'} quickselect@2.0.0: @@ -9941,9 +9457,9 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} raw-loader@4.0.2: resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} @@ -9965,8 +9481,8 @@ packages: peerDependencies: react: ^19.1.1 - react-email@4.2.8: - resolution: {integrity: sha512-Eqzs/xZnS881oghPO/4CQ1cULyESuUhEjfYboXmYNOokXnJ6QP5GKKJZ6zjkg9SnKXxSrIxSo5PxzCI5jReJMA==} + react-email@4.2.11: + resolution: {integrity: sha512-/7TXRgsTrXcV1u7kc5ZXDVlPvZqEBaYcflMhE2FgWIJh3OHLjj2FqctFTgYcp0iwzbR59a7gzJLmSKyD0wYJEQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -9992,24 +9508,9 @@ packages: react-loadable: '*' webpack: '>=4.41.1 || 5.x' - react-magic-dropzone@1.0.1: - resolution: {integrity: sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==} - react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} - react-redux@7.2.9: - resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} - peerDependencies: - react: ^16.8.3 || ^17 || ^18 - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - react-router-config@5.1.1: resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} peerDependencies: @@ -10059,10 +9560,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} - recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} @@ -10087,29 +9584,9 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - redux-devtools-extension@2.13.9: - resolution: {integrity: sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==} - deprecated: Package moved to @redux-devtools/extension. - peerDependencies: - redux: ^3.1.0 || ^4.0.0 - - redux-thunk@2.4.2: - resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} - peerDependencies: - redux: ^4 - - redux@4.2.1: - resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} - reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - refractor@4.9.0: - resolution: {integrity: sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==} - - reftools@1.1.9: - resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} - regenerate-unicode-properties@10.2.0: resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} engines: {node: '>=4'} @@ -10206,9 +9683,6 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - reselect@4.1.8: - resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} - resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -10264,11 +9738,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.0.1: - resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} - engines: {node: 20 || >=22} - hasBin: true - robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -10285,8 +9754,8 @@ packages: rollup: optional: true - rollup@4.46.3: - resolution: {integrity: sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==} + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -10391,11 +9860,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -10443,10 +9907,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha.js@2.4.11: - resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} - hasBin: true - shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -10458,8 +9918,8 @@ packages: resolution: {integrity: sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==} hasBin: true - sharp@0.34.2: - resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -10474,29 +9934,6 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} - hasBin: true - - should-equal@2.0.0: - resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} - - should-format@3.0.3: - resolution: {integrity: sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==} - - should-type-adaptors@1.1.0: - resolution: {integrity: sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==} - - should-type@1.4.0: - resolution: {integrity: sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==} - - should-util@1.0.1: - resolution: {integrity: sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==} - - should@13.2.3: - resolution: {integrity: sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==} - side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -10529,6 +9966,10 @@ packages: simple-get@3.1.1: resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + simple-icons@15.15.0: + resolution: {integrity: sha512-ohh1Uo9AjH10WN5wpPmtjnmbSLv6MjiULHS4dYA821uIsPAp0Q3XoluPnjBnQAFsztasmM6z2dezJBrQbtserg==} + engines: {node: '>=0.12.18'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -10536,8 +9977,8 @@ packages: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} - sirv@3.0.1: - resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} sisteransi@1.0.5: @@ -10638,14 +10079,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-formatter@15.6.6: - resolution: {integrity: sha512-bZydXEXhaNDQBr8xYHC3a8thwcaMuTBp0CkKGjwGYDsIB26tnlWeWPwJtSQ0TEwiJcz9iJJON5mFPkx7XroHcg==} + sql-formatter@15.6.9: + resolution: {integrity: sha512-r9VKnkRfKW7jbhTgytwbM+JqmFclQYN9L58Z3UTktuy9V1f1Y+rGK3t70Truh2wIOJzvZkzobAQ2PwGjjXsr6Q==} hasBin: true - sql-highlight@6.1.0: - resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} - engines: {node: '>=14'} - srcset@4.0.0: resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} engines: {node: '>=12'} @@ -10667,9 +10104,6 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - state-local@1.0.7: - resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -10728,8 +10162,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} strip-bom-string@1.0.0: @@ -10763,8 +10197,8 @@ packages: strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} - striptags@3.2.0: - resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} @@ -10791,11 +10225,6 @@ packages: resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} engines: {node: '>=14.18.0'} - superagent@7.1.6: - resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} - engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net - supercluster@7.1.5: resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} @@ -10826,8 +10255,8 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-eslint-parser@1.3.1: - resolution: {integrity: sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==} + svelte-eslint-parser@1.3.2: + resolution: {integrity: sha512-whla4VlUbwJidn/bNyC3Ho3pBrXnR2CBEkuJwtaURW+wfwgKHPaYtZAmwAkp6HWWKCw1ILZL6iKsFdVY11rpDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 @@ -10835,8 +10264,8 @@ packages: svelte: optional: true - svelte-gestures@5.1.4: - resolution: {integrity: sha512-gfSO/GqWLu9nRMCz12jqdyA0+NTsojYcIBcRqZjwWrpQbqMXr0zWPFpZBtzfYbRHtuFxZImMZp9MrVaFCYbhDg==} + svelte-gestures@5.2.2: + resolution: {integrity: sha512-Y+chXPaSx8OsPoFppUwPk8PJzgrZ7xoDJKXeiEc7JBqyKKzXer9hlf8F9O34eFuAWB4/WQEvccACvyBplESL7A==} svelte-i18n@4.0.1: resolution: {integrity: sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==} @@ -10845,8 +10274,8 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5 - svelte-maplibre@1.2.0: - resolution: {integrity: sha512-JKYzL0glnqCJ7LwkdDAMb3jdZdFl8ZDHEZyc043BV624kG9ZVaXlIPgjb8sNktqx1D0rQBNrYNt5rR4XszNCiQ==} + svelte-maplibre@1.2.1: + resolution: {integrity: sha512-IVkbc54hQXznyaiFN69RIdjqbLHriNYPVEo1DQMtWSm1kLovrt/aZuhV4eOoZKn6wIvY2Vz34jXPS33f/d/GNw==} peerDependencies: '@deck.gl/core': ^9 '@deck.gl/layers': ^9 @@ -10877,8 +10306,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.35.5: - resolution: {integrity: sha512-KuRvI82rhh0RMz1EKsUJD96gZyHJ+h2+8zrwO8iqE/p/CmcNKvIItDUAeUePhuCDgtegDJmF8IKThbHIfmTgTA==} + svelte@5.38.10: + resolution: {integrity: sha512-UY+OhrWK7WI22bCZ00P/M3HtyWgwJPi9IxSRkoAE2MeAy6kl7ZlZWJZ8RaB+X4KD/G+wjis+cGVnVYaoqbzBqg==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -10892,10 +10321,6 @@ packages: swagger-ui-dist@5.21.0: resolution: {integrity: sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==} - swagger2openapi@7.0.8: - resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} - hasBin: true - symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -10919,8 +10344,8 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - tailwind-variants@2.1.0: - resolution: {integrity: sha512-82m0eRex0z6A3GpvfoTCpHr+wWJmbecfVZfP3mqLoDxeya5tN4mYJQZwa5Aw1hRZTedwpu1D2JizYenoEdyD8w==} + tailwind-variants@3.1.1: + resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==} engines: {node: '>=16.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' @@ -10951,8 +10376,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.12: - resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} + tailwindcss@4.1.13: + resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} @@ -11021,12 +10446,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - three@0.175.0: - resolution: {integrity: sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==} - three@0.179.1: resolution: {integrity: sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw==} + three@0.180.0: + resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -11055,8 +10480,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} tinypool@1.1.1: @@ -11084,10 +10509,6 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -11216,67 +10637,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm@0.3.25: - resolution: {integrity: sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==} - engines: {node: '>=16.13.0'} - hasBin: true - peerDependencies: - '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 - '@sap/hana-client': ^2.12.25 - better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - hdb-pool: ^0.1.6 - ioredis: ^5.0.4 - mongodb: ^5.8.0 || ^6.0.0 - mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 - mysql2: ^2.2.5 || ^3.0.1 - oracledb: ^6.3.0 - pg: ^8.5.1 - pg-native: ^3.0.0 - pg-query-stream: ^4.0.0 - redis: ^3.1.1 || ^4.0.0 - reflect-metadata: ^0.1.14 || ^0.2.0 - sql.js: ^1.4.0 - sqlite3: ^5.0.3 - ts-node: ^10.7.0 - typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 - peerDependenciesMeta: - '@google-cloud/spanner': - optional: true - '@sap/hana-client': - optional: true - better-sqlite3: - optional: true - hdb-pool: - optional: true - ioredis: - optional: true - mongodb: - optional: true - mssql: - optional: true - mysql2: - optional: true - oracledb: - optional: true - pg: - optional: true - pg-native: - optional: true - pg-query-stream: - optional: true - redis: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - ts-node: - optional: true - typeorm-aurora-data-api-driver: - optional: true - - typescript-eslint@8.39.1: - resolution: {integrity: sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==} + typescript-eslint@8.43.0: + resolution: {integrity: sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -11295,8 +10657,8 @@ packages: ua-is-frozen@0.1.2: resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - ua-parser-js@2.0.4: - resolution: {integrity: sha512-XiBOnM/UpUq21ZZ91q2AVDOnGROE6UQd37WrO9WBgw4u2eGvUCNOheMmZ3EfEUj7DLHr8tre+Um/436Of/Vwzg==} + ua-parser-js@2.0.5: + resolution: {integrity: sha512-sZErtx3rhpvZQanWW5umau4o/snfoLqRcQwQIZ54377WtRzIecnIKvjpkd5JwPcSUMglGnbIgcsQBGAbdi3S9Q==} hasBin: true uglify-js@3.19.3: @@ -11319,17 +10681,14 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.10.0: - resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} - undici@7.14.0: - resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} + undici@7.16.0: + resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -11415,13 +10774,13 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-swc@1.5.5: - resolution: {integrity: sha512-BahYtYvQ/KSgOqHoy5FfQgp/oZNAB7jwERxNeFVeN/PtJhg4fpK/ybj9OwKtqGPseOadS7+TGbq6tH2DmDAYvA==} + unplugin-swc@1.5.7: + resolution: {integrity: sha512-Ng4uuLAodZToA0kQk3+oY8b0C/Q9oV0ohRMixH2nqWMhCF/wNuMYZXZznYpwRLmF7wC36TFIOywBAxCLOReoeg==} peerDependencies: '@swc/core': ^1.2.108 - unplugin@2.3.5: - resolution: {integrity: sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==} + unplugin@2.3.10: + resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} engines: {node: '>=18.12.0'} update-browserslist-db@1.1.3: @@ -11460,9 +10819,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.10.4: - resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} - utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -11494,21 +10850,6 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - validate.io-array@1.0.6: - resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} - - validate.io-function@1.0.2: - resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} - - validate.io-integer-array@1.0.0: - resolution: {integrity: sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==} - - validate.io-integer@1.0.5: - resolution: {integrity: sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==} - - validate.io-number@1.0.3: - resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==} - validator@13.15.15: resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} @@ -11555,8 +10896,8 @@ packages: vite: optional: true - vite@7.1.2: - resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==} + vite@7.1.5: + resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -11724,16 +11065,6 @@ packages: webpack-cli: optional: true - webpack@5.99.9: - resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - webpackbar@6.0.1: resolution: {integrity: sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==} engines: {node: '>=14.21.3'} @@ -11949,8 +11280,8 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} - yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} zimmerframe@1.1.2: @@ -12081,8 +11412,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@angular-devkit/core@19.2.15(chokidar@4.0.3)': dependencies: @@ -12095,11 +11426,11 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.15(@types/node@22.13.14)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.15(@types/node@22.18.5)(chokidar@4.0.3)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@22.13.14) + '@inquirer/prompts': 7.3.2(@types/node@22.18.5) ansi-colors: 4.1.3 symbol-observable: 4.0.0 yargs-parser: 21.1.1 @@ -12120,12 +11451,398 @@ snapshots: '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 optional: true + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.887.0 + '@aws-sdk/util-locate-window': 3.873.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.887.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-sesv2@3.890.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.890.0 + '@aws-sdk/credential-provider-node': 3.890.0 + '@aws-sdk/middleware-host-header': 3.887.0 + '@aws-sdk/middleware-logger': 3.887.0 + '@aws-sdk/middleware-recursion-detection': 3.887.0 + '@aws-sdk/middleware-user-agent': 3.890.0 + '@aws-sdk/region-config-resolver': 3.890.0 + '@aws-sdk/signature-v4-multi-region': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@aws-sdk/util-endpoints': 3.890.0 + '@aws-sdk/util-user-agent-browser': 3.887.0 + '@aws-sdk/util-user-agent-node': 3.890.0 + '@smithy/config-resolver': 4.2.2 + '@smithy/core': 3.11.0 + '@smithy/fetch-http-handler': 5.2.1 + '@smithy/hash-node': 4.1.1 + '@smithy/invalid-dependency': 4.1.1 + '@smithy/middleware-content-length': 4.1.1 + '@smithy/middleware-endpoint': 4.2.2 + '@smithy/middleware-retry': 4.2.2 + '@smithy/middleware-serde': 4.1.1 + '@smithy/middleware-stack': 4.1.1 + '@smithy/node-config-provider': 4.2.2 + '@smithy/node-http-handler': 4.2.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/url-parser': 4.1.1 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-body-length-node': 4.1.0 + '@smithy/util-defaults-mode-browser': 4.1.2 + '@smithy/util-defaults-mode-node': 4.1.2 + '@smithy/util-endpoints': 3.1.2 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-retry': 4.1.1 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.890.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.890.0 + '@aws-sdk/middleware-host-header': 3.887.0 + '@aws-sdk/middleware-logger': 3.887.0 + '@aws-sdk/middleware-recursion-detection': 3.887.0 + '@aws-sdk/middleware-user-agent': 3.890.0 + '@aws-sdk/region-config-resolver': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@aws-sdk/util-endpoints': 3.890.0 + '@aws-sdk/util-user-agent-browser': 3.887.0 + '@aws-sdk/util-user-agent-node': 3.890.0 + '@smithy/config-resolver': 4.2.2 + '@smithy/core': 3.11.0 + '@smithy/fetch-http-handler': 5.2.1 + '@smithy/hash-node': 4.1.1 + '@smithy/invalid-dependency': 4.1.1 + '@smithy/middleware-content-length': 4.1.1 + '@smithy/middleware-endpoint': 4.2.2 + '@smithy/middleware-retry': 4.2.2 + '@smithy/middleware-serde': 4.1.1 + '@smithy/middleware-stack': 4.1.1 + '@smithy/node-config-provider': 4.2.2 + '@smithy/node-http-handler': 4.2.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/url-parser': 4.1.1 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-body-length-node': 4.1.0 + '@smithy/util-defaults-mode-browser': 4.1.2 + '@smithy/util-defaults-mode-node': 4.1.2 + '@smithy/util-endpoints': 3.1.2 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-retry': 4.1.1 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.890.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@aws-sdk/xml-builder': 3.887.0 + '@smithy/core': 3.11.0 + '@smithy/node-config-provider': 4.2.2 + '@smithy/property-provider': 4.1.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/signature-v4': 5.2.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-utf8': 4.1.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/property-provider': 4.1.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/fetch-http-handler': 5.2.1 + '@smithy/node-http-handler': 4.2.1 + '@smithy/property-provider': 4.1.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/util-stream': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/credential-provider-env': 3.890.0 + '@aws-sdk/credential-provider-http': 3.890.0 + '@aws-sdk/credential-provider-process': 3.890.0 + '@aws-sdk/credential-provider-sso': 3.890.0 + '@aws-sdk/credential-provider-web-identity': 3.890.0 + '@aws-sdk/nested-clients': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/credential-provider-imds': 4.1.2 + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.890.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.890.0 + '@aws-sdk/credential-provider-http': 3.890.0 + '@aws-sdk/credential-provider-ini': 3.890.0 + '@aws-sdk/credential-provider-process': 3.890.0 + '@aws-sdk/credential-provider-sso': 3.890.0 + '@aws-sdk/credential-provider-web-identity': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/credential-provider-imds': 4.1.2 + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.890.0': + dependencies: + '@aws-sdk/client-sso': 3.890.0 + '@aws-sdk/core': 3.890.0 + '@aws-sdk/token-providers': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/nested-clients': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-host-header@3.887.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.887.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.887.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@aws/lambda-invoke-store': 0.0.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@aws-sdk/util-arn-parser': 3.873.0 + '@smithy/core': 3.11.0 + '@smithy/node-config-provider': 4.2.2 + '@smithy/protocol-http': 5.2.1 + '@smithy/signature-v4': 5.2.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/util-config-provider': 4.1.0 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-stream': 4.3.1 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@aws-sdk/util-endpoints': 3.890.0 + '@smithy/core': 3.11.0 + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.890.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.890.0 + '@aws-sdk/middleware-host-header': 3.887.0 + '@aws-sdk/middleware-logger': 3.887.0 + '@aws-sdk/middleware-recursion-detection': 3.887.0 + '@aws-sdk/middleware-user-agent': 3.890.0 + '@aws-sdk/region-config-resolver': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@aws-sdk/util-endpoints': 3.890.0 + '@aws-sdk/util-user-agent-browser': 3.887.0 + '@aws-sdk/util-user-agent-node': 3.890.0 + '@smithy/config-resolver': 4.2.2 + '@smithy/core': 3.11.0 + '@smithy/fetch-http-handler': 5.2.1 + '@smithy/hash-node': 4.1.1 + '@smithy/invalid-dependency': 4.1.1 + '@smithy/middleware-content-length': 4.1.1 + '@smithy/middleware-endpoint': 4.2.2 + '@smithy/middleware-retry': 4.2.2 + '@smithy/middleware-serde': 4.1.1 + '@smithy/middleware-stack': 4.1.1 + '@smithy/node-config-provider': 4.2.2 + '@smithy/node-http-handler': 4.2.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/url-parser': 4.1.1 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-body-length-node': 4.1.0 + '@smithy/util-defaults-mode-browser': 4.1.2 + '@smithy/util-defaults-mode-node': 4.1.2 + '@smithy/util-endpoints': 3.1.2 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-retry': 4.1.1 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.890.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@smithy/node-config-provider': 4.2.2 + '@smithy/types': 4.5.0 + '@smithy/util-config-provider': 4.1.0 + '@smithy/util-middleware': 4.1.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.890.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/protocol-http': 5.2.1 + '@smithy/signature-v4': 5.2.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.890.0': + dependencies: + '@aws-sdk/core': 3.890.0 + '@aws-sdk/nested-clients': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.887.0': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.873.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.890.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@smithy/types': 4.5.0 + '@smithy/url-parser': 4.1.1 + '@smithy/util-endpoints': 3.1.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.873.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.887.0': + dependencies: + '@aws-sdk/types': 3.887.0 + '@smithy/types': 4.5.0 + bowser: 2.12.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.890.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.890.0 + '@aws-sdk/types': 3.887.0 + '@smithy/node-config-provider': 4.2.2 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.887.0': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.0.1': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -12138,41 +11855,33 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) '@babel/helpers': 7.27.6 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.27.5': - dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.27.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 - jsesc: 3.1.0 - '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -12190,7 +11899,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.7) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -12207,7 +11916,7 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1 + debug: 4.4.3 lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -12217,15 +11926,15 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.27.7 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.7 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -12234,13 +11943,13 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@babel/helper-plugin-utils@7.27.1': {} @@ -12249,7 +11958,7 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12258,14 +11967,14 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.27.7 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -12278,29 +11987,25 @@ snapshots: '@babel/helper-wrap-function@7.27.1': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.27.7 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 - '@babel/parser@7.27.7': + '@babel/parser@7.28.4': dependencies: - '@babel/types': 7.27.7 - - '@babel/parser@7.28.3': - dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12327,7 +12032,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12376,7 +12081,7 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.7) - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12422,7 +12127,7 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.7) - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12437,7 +12142,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12486,7 +12191,7 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12532,7 +12237,7 @@ snapshots: '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12572,7 +12277,7 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-destructuring': 7.27.7(@babel/core@7.27.7) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.27.7) - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -12648,7 +12353,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -12818,7 +12523,7 @@ snapshots: babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.27.7) babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.7) babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.27.7) - core-js-compat: 3.43.0 + core-js-compat: 3.45.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -12827,7 +12532,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 esutils: 2.0.3 '@babel/preset-react@7.27.1(@babel/core@7.27.7)': @@ -12857,46 +12562,27 @@ snapshots: dependencies: core-js-pure: 3.43.0 - '@babel/runtime@7.27.6': {} - - '@babel/runtime@7.28.3': {} + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 - '@babel/traverse@7.27.7': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.28.3 - '@babel/template': 7.27.2 - '@babel/types': 7.27.7 - debug: 4.4.1 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/traverse@7.28.3': + '@babel/traverse@7.28.4': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - debug: 4.4.1 + '@babel/types': 7.28.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.27.7': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - - '@babel/types@7.28.2': + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -12915,16 +12601,16 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/color-helpers': 5.0.2 + '@csstools/color-helpers': 5.1.0 '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -12948,7 +12634,7 @@ snapshots: '@csstools/postcss-color-function@4.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -12957,7 +12643,7 @@ snapshots: '@csstools/postcss-color-mix-function@3.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -12966,7 +12652,7 @@ snapshots: '@csstools/postcss-color-mix-variadic-function-arguments@1.0.0(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -12996,14 +12682,14 @@ snapshots: '@csstools/postcss-gamut-mapping@2.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 postcss: 8.5.6 '@csstools/postcss-gradients-interpolation-method@5.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -13012,7 +12698,7 @@ snapshots: '@csstools/postcss-hwb-function@4.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -13095,7 +12781,7 @@ snapshots: '@csstools/postcss-oklab-function@4.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -13116,7 +12802,7 @@ snapshots: '@csstools/postcss-relative-color-syntax@3.0.10(postcss@8.5.6)': dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -13144,7 +12830,7 @@ snapshots: '@csstools/postcss-text-decoration-shorthand@4.0.2(postcss@8.5.6)': dependencies: - '@csstools/color-helpers': 5.0.2 + '@csstools/color-helpers': 5.1.0 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -13175,14 +12861,14 @@ snapshots: '@docsearch/css@3.9.0': {} - '@docsearch/react@3.9.0(@algolia/client-search@5.29.0)(@types/react@19.1.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': + '@docsearch/react@3.9.0(@algolia/client-search@5.29.0)(@types/react@19.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.29.0)(algoliasearch@5.29.0)(search-insights@2.17.3) '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.29.0)(algoliasearch@5.29.0) '@docsearch/css': 3.9.0 algoliasearch: 5.29.0 optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.13 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 @@ -13192,15 +12878,15 @@ snapshots: '@docusaurus/babel@3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/core': 7.27.7 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.3 '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.7) '@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.27.7) '@babel/preset-env': 7.27.2(@babel/core@7.27.7) '@babel/preset-react': 7.27.1(@babel/core@7.27.7) '@babel/preset-typescript': 7.27.1(@babel/core@7.27.7) - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@babel/runtime-corejs3': 7.27.6 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 '@docusaurus/logger': 3.8.1 '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) babel-plugin-dynamic-import-node: 2.3.3 @@ -13224,24 +12910,24 @@ snapshots: '@docusaurus/logger': 3.8.1 '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - babel-loader: 9.2.1(@babel/core@7.27.7)(webpack@5.99.9) + babel-loader: 9.2.1(@babel/core@7.27.7)(webpack@5.100.2) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.99.9) - css-loader: 6.11.0(webpack@5.99.9) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.99.9) + copy-webpack-plugin: 11.0.0(webpack@5.100.2) + css-loader: 6.11.0(webpack@5.100.2) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.100.2) cssnano: 6.1.2(postcss@8.5.6) - file-loader: 6.2.0(webpack@5.99.9) + file-loader: 6.2.0(webpack@5.100.2) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.2(webpack@5.99.9) - null-loader: 4.0.1(webpack@5.99.9) + mini-css-extract-plugin: 2.9.2(webpack@5.100.2) + null-loader: 4.0.1(webpack@5.100.2) postcss: 8.5.6 - postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.2)(webpack@5.99.9) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.2)(webpack@5.100.2) postcss-preset-env: 10.2.4(postcss@8.5.6) - terser-webpack-plugin: 5.3.14(webpack@5.99.9) + terser-webpack-plugin: 5.3.14(webpack@5.100.2) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9) - webpack: 5.99.9 - webpackbar: 6.0.1(webpack@5.99.9) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.100.2))(webpack@5.100.2) + webpack: 5.100.2 + webpackbar: 6.0.1(webpack@5.100.2) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -13258,7 +12944,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/core@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: '@docusaurus/babel': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/bundler': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) @@ -13267,7 +12953,7 @@ snapshots: '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.0(@types/react@19.1.10)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.1.13)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -13282,7 +12968,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.0 html-tags: 3.3.1 - html-webpack-plugin: 5.6.3(webpack@5.99.9) + html-webpack-plugin: 5.6.3(webpack@5.100.2) leven: 3.1.0 lodash: 4.17.21 open: 8.4.2 @@ -13292,7 +12978,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.99.9) + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.100.2) react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) @@ -13301,9 +12987,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.99.9 + webpack: 5.100.2 webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 4.15.2(webpack@5.99.9) + webpack-dev-server: 4.15.2(webpack@5.100.2) webpack-merge: 6.0.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13344,7 +13030,7 @@ snapshots: '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.4.0 - file-loader: 6.2.0(webpack@5.99.9) + file-loader: 6.2.0(webpack@5.100.2) fs-extra: 11.3.0 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -13360,9 +13046,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.100.2))(webpack@5.100.2) vfile: 6.0.3 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@swc/core' - acorn @@ -13375,7 +13061,7 @@ snapshots: dependencies: '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.1.8 + '@types/react': 19.1.13 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.3.1 @@ -13390,13 +13076,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-content-blog@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.1 '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13412,7 +13098,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13432,13 +13118,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.1 '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13453,7 +13139,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13473,9 +13159,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-content-pages@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13484,7 +13170,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13504,9 +13190,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-css-cascade-layers@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13532,9 +13218,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-debug@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.0 @@ -13561,9 +13247,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-google-analytics@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -13588,9 +13274,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-google-gtag@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.12 @@ -13616,9 +13302,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-google-tag-manager@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -13643,9 +13329,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-sitemap@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.1 '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13675,9 +13361,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-svgr@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13686,7 +13372,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13706,22 +13392,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': + '@docusaurus/preset-classic@3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(@types/react@19.1.13)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-pages': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-css-cascade-layers': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-debug': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-google-analytics': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-google-gtag': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-google-tag-manager': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-sitemap': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-svgr': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-classic': 3.8.1(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-pages': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-css-cascade-layers': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-debug': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-google-analytics': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-google-gtag': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-google-tag-manager': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-sitemap': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-svgr': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-classic': 3.8.1(@types/react@19.1.13)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(@types/react@19.1.13)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -13749,25 +13435,25 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.13 react: 18.3.1 - '@docusaurus/theme-classic@3.8.1(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/theme-classic@3.8.1(@types/react@19.1.13)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.1 '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-pages': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-pages': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.8.1 '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.0(@types/react@19.1.10)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.1.13)(react@18.3.1) clsx: 2.1.1 copy-text-to-clipboard: 3.2.0 infima: 0.2.0-alpha.45 @@ -13801,15 +13487,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.1.8 + '@types/react': 19.1.13 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -13826,13 +13512,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': + '@docusaurus/theme-search-algolia@3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(@types/react@19.1.13)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': dependencies: - '@docsearch/react': 3.9.0(@algolia/client-search@5.29.0)(@types/react@19.1.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docsearch/react': 3.9.0(@algolia/client-search@5.29.0)(@types/react@19.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.1 - '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.8.1 '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13879,14 +13565,14 @@ snapshots: dependencies: '@mdx-js/mdx': 3.1.0(acorn@8.15.0) '@types/history': 4.7.11 - '@types/react': 19.1.8 + '@types/react': 19.1.13 commander: 5.1.0 joi: 17.13.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.99.9 + webpack: 5.100.2 webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -13937,7 +13623,7 @@ snapshots: '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.99.9) + file-loader: 6.2.0(webpack@5.100.2) fs-extra: 11.3.0 github-slugger: 1.5.0 globby: 11.1.0 @@ -13950,9 +13636,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.100.2))(webpack@5.100.2) utility-types: 3.11.0 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@swc/core' - acorn @@ -13963,7 +13649,7 @@ snapshots: - uglify-js - webpack-cli - '@emnapi/runtime@1.4.5': + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 optional: true @@ -14115,14 +13801,9 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.5.1))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0(jiti@2.5.1))': dependencies: - eslint: 9.30.1(jiti@2.5.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))': - dependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.35.0(jiti@2.5.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -14130,21 +13811,13 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color '@eslint/config-helpers@0.3.1': {} - '@eslint/core@0.14.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/core@0.15.1': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/core@0.15.2': dependencies: '@types/json-schema': 7.0.15 @@ -14152,7 +13825,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -14163,27 +13836,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.30.1': {} - - '@eslint/js@9.33.0': {} + '@eslint/js@9.35.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.3': - dependencies: - '@eslint/core': 0.15.1 - levn: 0.4.1 - '@eslint/plugin-kit@0.3.5': dependencies: '@eslint/core': 0.15.2 levn: 0.4.1 - '@exodus/schemasafe@1.3.0': {} - - '@faker-js/faker@5.5.3': {} - - '@faker-js/faker@9.9.0': {} + '@faker-js/faker@10.0.0': {} '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: @@ -14194,7 +13856,7 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.3': + '@floating-ui/dom@1.7.4': dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 @@ -14205,7 +13867,7 @@ snapshots: dependencies: '@formatjs/fast-memoize': 2.2.7 '@formatjs/intl-localematcher': 0.6.1 - decimal.js: 10.5.0 + decimal.js: 10.6.0 tslib: 2.8.1 '@formatjs/fast-memoize@2.2.7': @@ -14253,130 +13915,134 @@ snapshots: '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.3': {} - '@img/sharp-darwin-arm64@0.34.2': + '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-arm64': 1.2.0 optional: true - '@img/sharp-darwin-x64@0.34.2': + '@img/sharp-darwin-x64@0.34.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 optional: true - '@img/sharp-libvips-darwin-arm64@1.1.0': + '@img/sharp-libvips-darwin-arm64@1.2.0': optional: true - '@img/sharp-libvips-darwin-x64@1.1.0': + '@img/sharp-libvips-darwin-x64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm64@1.1.0': + '@img/sharp-libvips-linux-arm64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm@1.1.0': + '@img/sharp-libvips-linux-arm@1.2.0': optional: true - '@img/sharp-libvips-linux-ppc64@1.1.0': + '@img/sharp-libvips-linux-ppc64@1.2.0': optional: true - '@img/sharp-libvips-linux-s390x@1.1.0': + '@img/sharp-libvips-linux-s390x@1.2.0': optional: true - '@img/sharp-libvips-linux-x64@1.1.0': + '@img/sharp-libvips-linux-x64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.1.0': + '@img/sharp-libvips-linuxmusl-x64@1.2.0': optional: true - '@img/sharp-linux-arm64@0.34.2': + '@img/sharp-linux-arm64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 optional: true - '@img/sharp-linux-arm@0.34.2': + '@img/sharp-linux-arm@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.2.0 optional: true - '@img/sharp-linux-s390x@0.34.2': + '@img/sharp-linux-ppc64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 optional: true - '@img/sharp-linux-x64@0.34.2': + '@img/sharp-linux-s390x@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 optional: true - '@img/sharp-linuxmusl-arm64@0.34.2': + '@img/sharp-linux-x64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.2.0 optional: true - '@img/sharp-linuxmusl-x64@0.34.2': + '@img/sharp-linuxmusl-arm64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 optional: true - '@img/sharp-wasm32@0.34.2': + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': dependencies: - '@emnapi/runtime': 1.4.5 + '@emnapi/runtime': 1.5.0 optional: true - '@img/sharp-win32-arm64@0.34.2': + '@img/sharp-win32-arm64@0.34.3': optional: true - '@img/sharp-win32-ia32@0.34.2': + '@img/sharp-win32-ia32@0.34.3': optional: true - '@img/sharp-win32-x64@0.34.2': + '@img/sharp-win32-x64@0.34.3': optional: true - '@immich/ui@0.24.1(@internationalized/date@3.8.2)(svelte@5.35.5)': + '@immich/ui@0.29.0(@internationalized/date@3.8.2)(svelte@5.38.10)': dependencies: '@mdi/js': 7.4.47 - bits-ui: 2.9.4(@internationalized/date@3.8.2)(svelte@5.35.5) - svelte: 5.35.5 + bits-ui: 2.9.8(@internationalized/date@3.8.2)(svelte@5.38.10) + simple-icons: 15.15.0 + svelte: 5.38.10 tailwind-merge: 3.3.1 - tailwind-variants: 2.1.0(tailwind-merge@3.3.1)(tailwindcss@4.1.12) - tailwindcss: 4.1.12 + tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.13) + tailwindcss: 4.1.13 transitivePeerDependencies: - '@internationalized/date' - '@inquirer/checkbox@4.2.1(@types/node@22.13.14)': + '@inquirer/checkbox@4.2.1(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.5) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/confirm@5.1.15(@types/node@22.13.14)': + '@inquirer/confirm@5.1.15(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/core@10.1.15(@types/node@22.13.14)': + '@inquirer/core@10.1.15(@types/node@22.18.5)': dependencies: '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.5) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -14384,115 +14050,115 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/editor@4.2.17(@types/node@22.13.14)': + '@inquirer/editor@4.2.17(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/external-editor': 1.0.1(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/external-editor': 1.0.2(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/expand@4.0.17(@types/node@22.13.14)': + '@inquirer/expand@4.0.17(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/external-editor@1.0.1(@types/node@22.13.14)': + '@inquirer/external-editor@1.0.2(@types/node@22.18.5)': dependencies: chardet: 2.1.0 - iconv-lite: 0.6.3 + iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 '@inquirer/figures@1.0.13': {} - '@inquirer/input@4.2.1(@types/node@22.13.14)': + '@inquirer/input@4.2.1(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/number@3.0.17(@types/node@22.13.14)': + '@inquirer/number@3.0.17(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/password@4.0.17(@types/node@22.13.14)': + '@inquirer/password@4.0.17(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) ansi-escapes: 4.3.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/prompts@7.3.2(@types/node@22.13.14)': + '@inquirer/prompts@7.3.2(@types/node@22.18.5)': dependencies: - '@inquirer/checkbox': 4.2.1(@types/node@22.13.14) - '@inquirer/confirm': 5.1.15(@types/node@22.13.14) - '@inquirer/editor': 4.2.17(@types/node@22.13.14) - '@inquirer/expand': 4.0.17(@types/node@22.13.14) - '@inquirer/input': 4.2.1(@types/node@22.13.14) - '@inquirer/number': 3.0.17(@types/node@22.13.14) - '@inquirer/password': 4.0.17(@types/node@22.13.14) - '@inquirer/rawlist': 4.1.5(@types/node@22.13.14) - '@inquirer/search': 3.1.0(@types/node@22.13.14) - '@inquirer/select': 4.3.1(@types/node@22.13.14) + '@inquirer/checkbox': 4.2.1(@types/node@22.18.5) + '@inquirer/confirm': 5.1.15(@types/node@22.18.5) + '@inquirer/editor': 4.2.17(@types/node@22.18.5) + '@inquirer/expand': 4.0.17(@types/node@22.18.5) + '@inquirer/input': 4.2.1(@types/node@22.18.5) + '@inquirer/number': 3.0.17(@types/node@22.18.5) + '@inquirer/password': 4.0.17(@types/node@22.18.5) + '@inquirer/rawlist': 4.1.5(@types/node@22.18.5) + '@inquirer/search': 3.1.0(@types/node@22.18.5) + '@inquirer/select': 4.3.1(@types/node@22.18.5) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/prompts@7.8.0(@types/node@22.13.14)': + '@inquirer/prompts@7.8.0(@types/node@22.18.5)': dependencies: - '@inquirer/checkbox': 4.2.1(@types/node@22.13.14) - '@inquirer/confirm': 5.1.15(@types/node@22.13.14) - '@inquirer/editor': 4.2.17(@types/node@22.13.14) - '@inquirer/expand': 4.0.17(@types/node@22.13.14) - '@inquirer/input': 4.2.1(@types/node@22.13.14) - '@inquirer/number': 3.0.17(@types/node@22.13.14) - '@inquirer/password': 4.0.17(@types/node@22.13.14) - '@inquirer/rawlist': 4.1.5(@types/node@22.13.14) - '@inquirer/search': 3.1.0(@types/node@22.13.14) - '@inquirer/select': 4.3.1(@types/node@22.13.14) + '@inquirer/checkbox': 4.2.1(@types/node@22.18.5) + '@inquirer/confirm': 5.1.15(@types/node@22.18.5) + '@inquirer/editor': 4.2.17(@types/node@22.18.5) + '@inquirer/expand': 4.0.17(@types/node@22.18.5) + '@inquirer/input': 4.2.1(@types/node@22.18.5) + '@inquirer/number': 3.0.17(@types/node@22.18.5) + '@inquirer/password': 4.0.17(@types/node@22.18.5) + '@inquirer/rawlist': 4.1.5(@types/node@22.18.5) + '@inquirer/search': 3.1.0(@types/node@22.18.5) + '@inquirer/select': 4.3.1(@types/node@22.18.5) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/rawlist@4.1.5(@types/node@22.13.14)': + '@inquirer/rawlist@4.1.5(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) + '@inquirer/type': 3.0.8(@types/node@22.18.5) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/search@3.1.0(@types/node@22.13.14)': + '@inquirer/search@3.1.0(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.5) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/select@4.3.1(@types/node@22.13.14)': + '@inquirer/select@4.3.1(@types/node@22.18.5)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.5) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.5) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 - '@inquirer/type@3.0.8(@types/node@22.13.14)': + '@inquirer/type@3.0.8(@types/node@22.18.5)': optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 '@internationalized/date@3.8.2': dependencies: @@ -14510,7 +14176,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -14530,7 +14196,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -14539,12 +14205,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -14552,22 +14212,13 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.6': dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -14581,19 +14232,19 @@ snapshots: '@koa/router@14.0.0': dependencies: - debug: 4.4.1 + debug: 4.4.3 http-errors: 2.0.0 koa-compose: 4.1.0 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 transitivePeerDependencies: - supports-color - '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': + '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@mdn/browser-compat-data': 6.0.27 - '@typescript-eslint/type-utils': 8.35.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - browserslist: 4.25.1 + '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + browserslist: 4.25.3 transitivePeerDependencies: - eslint - supports-color @@ -14622,7 +14273,7 @@ snapshots: '@mapbox/node-pre-gyp@1.0.11': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.0 https-proxy-agent: 5.0.1 make-dir: 3.1.0 node-fetch: 2.7.0 @@ -14638,7 +14289,7 @@ snapshots: '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.0 https-proxy-agent: 5.0.1 make-dir: 3.1.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -14735,25 +14386,14 @@ snapshots: - acorn - supports-color - '@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1)': + '@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.1.10 + '@types/react': 19.1.13 react: 18.3.1 '@microsoft/tsdoc@0.15.1': {} - '@monaco-editor/loader@1.5.0': - dependencies: - state-local: 1.0.7 - - '@monaco-editor/react@4.7.0(monaco-editor@0.31.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@monaco-editor/loader': 1.5.0 - monaco-editor: 0.31.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -14780,26 +14420,26 @@ snapshots: '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.57.0)': + '@nestjs/bullmq@11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.58.5)': dependencies: '@nestjs/bull-shared': 11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.57.0 + bullmq: 5.58.5 tslib: 2.8.1 - '@nestjs/cli@11.0.10(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/node@22.13.14)': + '@nestjs/cli@11.0.10(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.5)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.15(@types/node@22.13.14)(chokidar@4.0.3) - '@inquirer/prompts': 7.8.0(@types/node@22.13.14) + '@angular-devkit/schematics-cli': 19.2.15(@types/node@22.18.5)(chokidar@4.0.3) + '@inquirer/prompts': 7.8.0(@types/node@22.18.5) '@nestjs/schematics': 11.0.7(chokidar@4.0.3)(typescript@5.8.3) ansis: 4.1.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17))) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))) glob: 11.0.3 node-emoji: 1.11.0 ora: 5.4.1 @@ -14807,10 +14447,10 @@ snapshots: tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.8.3 - webpack: 5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17)) + webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.13.3(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.17) transitivePeerDependencies: - '@types/node' - esbuild @@ -14847,12 +14487,6 @@ snapshots: '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) '@nestjs/websockets': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': - dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) - eventemitter2: 6.4.9 - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -14982,712 +14616,311 @@ snapshots: '@oazapfts/runtime@1.0.4': {} - '@opentelemetry/api-logs@0.203.0': + '@opentelemetry/api-logs@0.205.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/auto-instrumentations-node@0.62.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-aws-lambda': 0.54.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-aws-sdk': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-bunyan': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-cassandra-driver': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-cucumber': 0.18.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.21.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dns': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fastify': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.23.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.13.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-memcached': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-nestjs-core': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-net': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-oracledb': 0.29.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pino': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-restify': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-router': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-runtime-node': 0.17.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-socket.io': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.14.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-winston': 0.48.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-alibaba-cloud': 0.31.3(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-aws': 2.3.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-azure': 0.10.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-container': 0.7.3(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-gcp': 0.37.0(@opentelemetry/api@1.9.0)(encoding@0.1.13) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - encoding - - supports-color - - '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.13.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.13.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.13.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 '@opentelemetry/host-metrics@0.36.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-aws-lambda@0.54.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@types/aws-lambda': 8.10.150 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-aws-sdk@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagation-utils': 0.31.3(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-bunyan@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@types/bunyan': 1.8.11 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-cassandra-driver@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-connect@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@types/connect': 3.4.38 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-cucumber@0.18.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dataloader@0.21.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dns@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-express@0.52.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fastify@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fs@0.23.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-generic-pool@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-graphql@0.51.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-grpc@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-hapi@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-http@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.51.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.53.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.205.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.0 - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-kafkajs@0.13.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.51.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/instrumentation': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-knex@0.48.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.58.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-koa@0.51.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-lru-memoizer@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-memcached@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@types/memcached': 2.2.10 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongodb@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongoose@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql2@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 '@opentelemetry/sql-common': 0.41.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@types/mysql': 2.15.27 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-nestjs-core@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-net@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-oracledb@0.29.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@types/oracledb': 6.5.2 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-pg@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@opentelemetry/sql-common': 0.41.0(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.4 + '@types/pg': 8.15.5 '@types/pg-pool': 2.0.6 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pino@0.50.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-redis@0.51.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.0 - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-restify@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-router@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-runtime-node@0.17.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-socket.io@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-tedious@0.22.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-undici@0.14.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-winston@0.48.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/api-logs': 0.205.0 import-in-the-middle: 1.14.2 require-in-the-middle: 7.5.2 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.13.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 - '@opentelemetry/propagation-utils@0.31.3(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common@0.38.0': {} - '@opentelemetry/resource-detector-alibaba-cloud@0.31.3(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/resource-detector-aws@2.3.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-azure@0.10.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-container@0.7.3(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.205.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - - '@opentelemetry/resource-detector-gcp@0.37.0(@opentelemetry/api@1.9.0)(encoding@0.1.13)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - gcp-metadata: 6.1.1(encoding@0.1.13) - transitivePeerDependencies: - - encoding - - supports-color - - '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 - - '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-node@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions@1.36.0': {} + '@opentelemetry/semantic-conventions@1.37.0': {} '@opentelemetry/sql-common@0.41.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2@2.2.2': dependencies: '@noble/hashes': 1.8.0 - '@photo-sphere-viewer/core@5.13.4': + '@photo-sphere-viewer/core@5.14.0': dependencies: - three: 0.175.0 - - '@photo-sphere-viewer/equirectangular-video-adapter@5.13.4(@photo-sphere-viewer/core@5.13.4)(@photo-sphere-viewer/video-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4))': - dependencies: - '@photo-sphere-viewer/core': 5.13.4 - '@photo-sphere-viewer/video-plugin': 5.13.4(@photo-sphere-viewer/core@5.13.4) three: 0.179.1 - '@photo-sphere-viewer/resolution-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4)(@photo-sphere-viewer/settings-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4))': + '@photo-sphere-viewer/equirectangular-video-adapter@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': dependencies: - '@photo-sphere-viewer/core': 5.13.4 - '@photo-sphere-viewer/settings-plugin': 5.13.4(@photo-sphere-viewer/core@5.13.4) + '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/video-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) + three: 0.180.0 - '@photo-sphere-viewer/settings-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4)': + '@photo-sphere-viewer/resolution-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': dependencies: - '@photo-sphere-viewer/core': 5.13.4 + '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/settings-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) - '@photo-sphere-viewer/video-plugin@5.13.4(@photo-sphere-viewer/core@5.13.4)': + '@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': dependencies: - '@photo-sphere-viewer/core': 5.13.4 - three: 0.179.1 + '@photo-sphere-viewer/core': 5.14.0 + + '@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + dependencies: + '@photo-sphere-viewer/core': 5.14.0 + three: 0.180.0 '@photostructure/tz-lookup@11.2.0': {} @@ -15696,9 +14929,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.54.2': + '@playwright/test@1.55.0': dependencies: - playwright: 1.54.2 + playwright: 1.55.0 '@pnpm/config.env-replace@1.1.0': {} @@ -15758,7 +14991,7 @@ snapshots: dependencies: react: 19.1.1 - '@react-email/components@0.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@react-email/components@0.5.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@react-email/body': 0.1.0(react@19.1.1) '@react-email/button': 0.2.0(react@19.1.1) @@ -15775,7 +15008,7 @@ snapshots: '@react-email/link': 0.0.12(react@19.1.1) '@react-email/markdown': 0.0.15(react@19.1.1) '@react-email/preview': 0.0.13(react@19.1.1) - '@react-email/render': 1.2.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-email/render': 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-email/row': 0.0.12(react@19.1.1) '@react-email/section': 0.0.16(react@19.1.1) '@react-email/tailwind': 1.2.2(react@19.1.1) @@ -15825,7 +15058,7 @@ snapshots: dependencies: react: 19.1.1 - '@react-email/render@1.2.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@react-email/render@1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: html-to-text: 9.0.5 prettier: 3.6.2 @@ -15849,82 +15082,75 @@ snapshots: dependencies: react: 19.1.1 - '@reduxjs/toolkit@1.9.7(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': - dependencies: - immer: 9.0.21 - redux: 4.2.1 - redux-thunk: 2.4.2(redux@4.2.1) - reselect: 4.1.8 - optionalDependencies: - react: 18.3.1 - react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@rollup/pluginutils@5.2.0(rollup@4.46.3)': + '@rollup/pluginutils@5.3.0(rollup@4.50.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.46.3 + rollup: 4.50.1 - '@rollup/rollup-android-arm-eabi@4.46.3': + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true - '@rollup/rollup-android-arm64@4.46.3': + '@rollup/rollup-android-arm64@4.50.1': optional: true - '@rollup/rollup-darwin-arm64@4.46.3': + '@rollup/rollup-darwin-arm64@4.50.1': optional: true - '@rollup/rollup-darwin-x64@4.46.3': + '@rollup/rollup-darwin-x64@4.50.1': optional: true - '@rollup/rollup-freebsd-arm64@4.46.3': + '@rollup/rollup-freebsd-arm64@4.50.1': optional: true - '@rollup/rollup-freebsd-x64@4.46.3': + '@rollup/rollup-freebsd-x64@4.50.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.3': + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.3': + '@rollup/rollup-linux-arm-musleabihf@4.50.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.3': + '@rollup/rollup-linux-arm64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.3': + '@rollup/rollup-linux-arm64-musl@4.50.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.3': + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.3': + '@rollup/rollup-linux-ppc64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.3': + '@rollup/rollup-linux-riscv64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.3': + '@rollup/rollup-linux-riscv64-musl@4.50.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.3': + '@rollup/rollup-linux-s390x-gnu@4.50.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.3': + '@rollup/rollup-linux-x64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-x64-musl@4.46.3': + '@rollup/rollup-linux-x64-musl@4.50.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.3': + '@rollup/rollup-openharmony-arm64@4.50.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.3': + '@rollup/rollup-win32-arm64-msvc@4.50.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.3': + '@rollup/rollup-win32-ia32-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true '@scarf/scarf@1.4.0': {} @@ -15950,7 +15176,7 @@ snapshots: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 invariant: 2.2.4 prop-types: 15.8.1 react: 18.3.1 @@ -15964,6 +15190,278 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 + '@smithy/abort-controller@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/config-resolver@4.2.2': + dependencies: + '@smithy/node-config-provider': 4.2.2 + '@smithy/types': 4.5.0 + '@smithy/util-config-provider': 4.1.0 + '@smithy/util-middleware': 4.1.1 + tslib: 2.8.1 + + '@smithy/core@3.11.0': + dependencies: + '@smithy/middleware-serde': 4.1.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-stream': 4.3.1 + '@smithy/util-utf8': 4.1.0 + '@types/uuid': 9.0.8 + tslib: 2.8.1 + uuid: 9.0.1 + + '@smithy/credential-provider-imds@4.1.2': + dependencies: + '@smithy/node-config-provider': 4.2.2 + '@smithy/property-provider': 4.1.1 + '@smithy/types': 4.5.0 + '@smithy/url-parser': 4.1.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.2.1': + dependencies: + '@smithy/protocol-http': 5.2.1 + '@smithy/querystring-builder': 4.1.1 + '@smithy/types': 4.5.0 + '@smithy/util-base64': 4.1.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + '@smithy/util-buffer-from': 4.1.0 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.1.0': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.1.1': + dependencies: + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.2.2': + dependencies: + '@smithy/core': 3.11.0 + '@smithy/middleware-serde': 4.1.1 + '@smithy/node-config-provider': 4.2.2 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + '@smithy/url-parser': 4.1.1 + '@smithy/util-middleware': 4.1.1 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.2.2': + dependencies: + '@smithy/node-config-provider': 4.2.2 + '@smithy/protocol-http': 5.2.1 + '@smithy/service-error-classification': 4.1.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-retry': 4.1.1 + '@types/uuid': 9.0.8 + tslib: 2.8.1 + uuid: 9.0.1 + + '@smithy/middleware-serde@4.1.1': + dependencies: + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.2.2': + dependencies: + '@smithy/property-provider': 4.1.1 + '@smithy/shared-ini-file-loader': 4.2.0 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.2.1': + dependencies: + '@smithy/abort-controller': 4.1.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/querystring-builder': 4.1.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.2.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + '@smithy/util-uri-escape': 4.1.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + + '@smithy/shared-ini-file-loader@4.2.0': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.2.1': + dependencies: + '@smithy/is-array-buffer': 4.1.0 + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + '@smithy/util-hex-encoding': 4.1.0 + '@smithy/util-middleware': 4.1.1 + '@smithy/util-uri-escape': 4.1.0 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.6.2': + dependencies: + '@smithy/core': 3.11.0 + '@smithy/middleware-endpoint': 4.2.2 + '@smithy/middleware-stack': 4.1.1 + '@smithy/protocol-http': 5.2.1 + '@smithy/types': 4.5.0 + '@smithy/util-stream': 4.3.1 + tslib: 2.8.1 + + '@smithy/types@4.5.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.1.1': + dependencies: + '@smithy/querystring-parser': 4.1.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.1.0': + dependencies: + '@smithy/util-buffer-from': 4.1.0 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.1.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.1.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.1.0': + dependencies: + '@smithy/is-array-buffer': 4.1.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.1.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.1.2': + dependencies: + '@smithy/property-provider': 4.1.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + bowser: 2.12.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.1.2': + dependencies: + '@smithy/config-resolver': 4.2.2 + '@smithy/credential-provider-imds': 4.1.2 + '@smithy/node-config-provider': 4.2.2 + '@smithy/property-provider': 4.1.1 + '@smithy/smithy-client': 4.6.2 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.1.2': + dependencies: + '@smithy/node-config-provider': 4.2.2 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.1.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.1.1': + dependencies: + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.1.1': + dependencies: + '@smithy/service-error-classification': 4.1.1 + '@smithy/types': 4.5.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.3.1': + dependencies: + '@smithy/fetch-http-handler': 5.2.1 + '@smithy/node-http-handler': 4.2.1 + '@smithy/types': 4.5.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-buffer-from': 4.1.0 + '@smithy/util-hex-encoding': 4.1.0 + '@smithy/util-utf8': 4.1.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.1.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.1.0': + dependencies: + '@smithy/util-buffer-from': 4.1.0 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.5)': @@ -15975,70 +15473,69 @@ snapshots: transitivePeerDependencies: - supports-color - '@sqltools/formatter@1.2.5': {} - '@standard-schema/spec@1.0.0': {} '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.9(@sveltejs/kit@2.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))': + '@sveltejs/adapter-static@3.0.9(@sveltejs/kit@2.38.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))': dependencies: - '@sveltejs/kit': 2.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@sveltejs/kit': 2.38.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) - '@sveltejs/enhanced-img@0.8.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(rollup@4.46.3)(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@sveltejs/enhanced-img@0.8.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(rollup@4.50.1)(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) - magic-string: 0.30.17 - sharp: 0.34.2 - svelte: 5.35.5 - svelte-parse-markup: 0.1.5(svelte@5.35.5) - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vite-imagetools: 8.0.0(rollup@4.46.3) + '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + magic-string: 0.30.19 + sharp: 0.34.3 + svelte: 5.38.10 + svelte-parse-markup: 0.1.5(svelte@5.38.10) + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite-imagetools: 8.0.0(rollup@4.50.1) zimmerframe: 1.1.2 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@sveltejs/kit@2.38.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.1.1 + devalue: 5.3.2 esm-env: 1.2.2 kleur: 4.1.5 - magic-string: 0.30.17 + magic-string: 0.30.19 mrmime: 2.0.1 sade: 1.8.1 set-cookie-parser: 2.7.1 - sirv: 3.0.1 - svelte: 5.35.5 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + sirv: 3.0.2 + svelte: 5.38.10 + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + optionalDependencies: + '@opentelemetry/api': 1.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) - debug: 4.4.1 - svelte: 5.35.5 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + debug: 4.4.3 + svelte: 5.38.10 + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) - debug: 4.4.1 + '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.2.0(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + debug: 4.4.3 deepmerge: 4.3.1 - kleur: 4.1.5 - magic-string: 0.30.17 - svelte: 5.35.5 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + magic-string: 0.30.19 + svelte: 5.38.10 + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) transitivePeerDependencies: - supports-color @@ -16099,7 +15596,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))': @@ -16135,51 +15632,51 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.13.3': + '@swc/core-darwin-arm64@1.13.5': optional: true - '@swc/core-darwin-x64@1.13.3': + '@swc/core-darwin-x64@1.13.5': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.3': + '@swc/core-linux-arm-gnueabihf@1.13.5': optional: true - '@swc/core-linux-arm64-gnu@1.13.3': + '@swc/core-linux-arm64-gnu@1.13.5': optional: true - '@swc/core-linux-arm64-musl@1.13.3': + '@swc/core-linux-arm64-musl@1.13.5': optional: true - '@swc/core-linux-x64-gnu@1.13.3': + '@swc/core-linux-x64-gnu@1.13.5': optional: true - '@swc/core-linux-x64-musl@1.13.3': + '@swc/core-linux-x64-musl@1.13.5': optional: true - '@swc/core-win32-arm64-msvc@1.13.3': + '@swc/core-win32-arm64-msvc@1.13.5': optional: true - '@swc/core-win32-ia32-msvc@1.13.3': + '@swc/core-win32-ia32-msvc@1.13.5': optional: true - '@swc/core-win32-x64-msvc@1.13.3': + '@swc/core-win32-x64-msvc@1.13.5': optional: true - '@swc/core@1.13.3(@swc/helpers@0.5.17)': + '@swc/core@1.13.5(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.24 + '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.3 - '@swc/core-darwin-x64': 1.13.3 - '@swc/core-linux-arm-gnueabihf': 1.13.3 - '@swc/core-linux-arm64-gnu': 1.13.3 - '@swc/core-linux-arm64-musl': 1.13.3 - '@swc/core-linux-x64-gnu': 1.13.3 - '@swc/core-linux-x64-musl': 1.13.3 - '@swc/core-win32-arm64-msvc': 1.13.3 - '@swc/core-win32-ia32-msvc': 1.13.3 - '@swc/core-win32-x64-msvc': 1.13.3 + '@swc/core-darwin-arm64': 1.13.5 + '@swc/core-darwin-x64': 1.13.5 + '@swc/core-linux-arm-gnueabihf': 1.13.5 + '@swc/core-linux-arm64-gnu': 1.13.5 + '@swc/core-linux-arm64-musl': 1.13.5 + '@swc/core-linux-x64-gnu': 1.13.5 + '@swc/core-linux-x64-musl': 1.13.5 + '@swc/core-win32-arm64-msvc': 1.13.5 + '@swc/core-win32-ia32-msvc': 1.13.5 + '@swc/core-win32-x64-msvc': 1.13.5 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -16188,7 +15685,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/types@0.1.24': + '@swc/types@0.1.25': dependencies: '@swc/counter': 0.1.3 @@ -16196,95 +15693,81 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.1.12': + '@tailwindcss/node@4.1.13': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.18.3 jiti: 2.5.1 lightningcss: 1.30.1 - magic-string: 0.30.17 + magic-string: 0.30.19 source-map-js: 1.2.1 - tailwindcss: 4.1.12 + tailwindcss: 4.1.13 - '@tailwindcss/oxide-android-arm64@4.1.12': + '@tailwindcss/oxide-android-arm64@4.1.13': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.12': + '@tailwindcss/oxide-darwin-arm64@4.1.13': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.12': + '@tailwindcss/oxide-darwin-x64@4.1.13': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.12': + '@tailwindcss/oxide-freebsd-x64@4.1.13': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.12': + '@tailwindcss/oxide-linux-x64-musl@4.1.13': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.12': + '@tailwindcss/oxide-wasm32-wasi@4.1.13': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': optional: true - '@tailwindcss/oxide@4.1.12': + '@tailwindcss/oxide@4.1.13': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.0 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.12 - '@tailwindcss/oxide-darwin-arm64': 4.1.12 - '@tailwindcss/oxide-darwin-x64': 4.1.12 - '@tailwindcss/oxide-freebsd-x64': 4.1.12 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.12 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.12 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.12 - '@tailwindcss/oxide-linux-x64-musl': 4.1.12 - '@tailwindcss/oxide-wasm32-wasi': 4.1.12 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + '@tailwindcss/oxide-android-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-x64': 4.1.13 + '@tailwindcss/oxide-freebsd-x64': 4.1.13 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-x64-musl': 4.1.13 + '@tailwindcss/oxide-wasm32-wasi': 4.1.13 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 - '@tailwindcss/vite@4.1.12(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.13(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: - '@tailwindcss/node': 4.1.12 - '@tailwindcss/oxide': 4.1.12 - tailwindcss: 4.1.12 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - - '@testcontainers/postgresql@11.5.1': - dependencies: - testcontainers: 11.5.1 - transitivePeerDependencies: - - bare-buffer - - supports-color - - '@testcontainers/redis@11.5.1': - dependencies: - testcontainers: 11.5.1 - transitivePeerDependencies: - - bare-buffer - - supports-color + '@tailwindcss/node': 4.1.13 + '@tailwindcss/oxide': 4.1.13 + tailwindcss: 4.1.13 + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -16292,7 +15775,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.7.0': + '@testing-library/jest-dom@6.8.0': dependencies: '@adobe/css-tools': 4.4.4 aria-query: 5.3.2 @@ -16301,13 +15784,13 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte@5.2.8(svelte@5.35.5)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@testing-library/svelte@5.2.8(svelte@5.38.10)(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@testing-library/dom': 10.4.0 - svelte: 5.35.5 + svelte: 5.38.10 optionalDependencies: - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: @@ -16315,7 +15798,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.1 + debug: 4.4.3 fflate: 0.8.2 token-types: 6.1.1 transitivePeerDependencies: @@ -16349,7 +15832,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/archiver@6.0.3': dependencies: @@ -16359,27 +15842,21 @@ snapshots: '@types/async-lock@1.4.2': {} - '@types/aws-lambda@8.10.150': {} - '@types/bcrypt@6.0.0': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/braces@3.0.5': {} - '@types/bunyan@1.8.11': - dependencies: - '@types/node': 22.17.2 - '@types/byte-size@8.1.2': {} '@types/chai@5.2.2': @@ -16397,21 +15874,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.3 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.0.6 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/connect@3.4.38': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/content-disposition@0.5.9': {} @@ -16428,11 +15905,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.3 '@types/keygrip': 1.0.6 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/cors@2.8.19': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/debug@4.1.12': dependencies: @@ -16442,13 +15919,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ssh2': 1.15.5 '@types/dockerode@3.3.42': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -16471,14 +15948,14 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 @@ -16504,7 +15981,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.27': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/geojson-vt@3.2.5': dependencies: @@ -16526,11 +16003,6 @@ snapshots: '@types/history@4.7.11': {} - '@types/hoist-non-react-statics@3.3.6': - dependencies: - '@types/react': 19.1.10 - hoist-non-react-statics: 3.3.2 - '@types/html-minifier-terser@6.1.0': {} '@types/http-assert@1.5.6': {} @@ -16541,7 +16013,7 @@ snapshots: '@types/http-proxy@1.17.16': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/inquirer@8.2.11': dependencies: @@ -16579,17 +16051,15 @@ snapshots: '@types/http-errors': 2.0.5 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 22.17.2 + '@types/node': 22.18.5 - '@types/leaflet@1.9.19': + '@types/leaflet@1.9.20': dependencies: '@types/geojson': 7946.0.16 '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.19 - - '@types/lodash@4.17.19': {} + '@types/lodash': 4.17.20 '@types/lodash@4.17.20': {} @@ -16603,10 +16073,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/memcached@2.2.10': - dependencies: - '@types/node': 22.17.2 - '@types/methods@1.1.4': {} '@types/micromatch@4.0.9': @@ -16617,7 +16083,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ms@2.1.0': {} @@ -16625,22 +16091,13 @@ snapshots: dependencies: '@types/express': 5.0.3 - '@types/mysql@2.15.27': - dependencies: - '@types/node': 22.17.2 - - '@types/node-fetch@2.6.12': - dependencies: - '@types/node': 22.17.2 - form-data: 4.0.3 - '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/node@17.0.45': {} - '@types/node@18.19.123': + '@types/node@18.19.126': dependencies: undici-types: 5.26.5 @@ -16648,48 +16105,37 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.13.14': - dependencies: - undici-types: 6.20.0 - - '@types/node@22.17.2': + '@types/node@22.18.5': dependencies: undici-types: 6.21.0 - '@types/node@24.3.0': + '@types/node@24.5.1': dependencies: - undici-types: 7.10.0 + undici-types: 7.12.0 optional: true - '@types/nodemailer@6.4.17': + '@types/nodemailer@7.0.1': dependencies: - '@types/node': 22.17.2 + '@aws-sdk/client-sesv2': 3.890.0 + '@types/node': 22.18.5 + transitivePeerDependencies: + - aws-crt - '@types/oidc-provider@9.1.2': + '@types/oidc-provider@9.5.0': dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.0 - '@types/node': 22.17.2 - - '@types/oracledb@6.5.2': - dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/parse5@5.0.3': {} '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.15.4 - - '@types/pg@8.15.4': - dependencies: - '@types/node': 22.17.2 - pg-protocol: 1.10.3 - pg-types: 2.2.0 + '@types/pg': 8.15.5 '@types/pg@8.15.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -16697,53 +16143,42 @@ snapshots: '@types/pngjs@6.0.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} - '@types/react-redux@7.1.34': - dependencies: - '@types/hoist-non-react-statics': 3.3.6 - '@types/react': 19.1.10 - hoist-non-react-statics: 3.3.2 - redux: 4.2.1 - '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.8 + '@types/react': 19.1.13 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.8 + '@types/react': 19.1.13 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.8 + '@types/react': 19.1.13 - '@types/react@19.1.10': - dependencies: - csstype: 3.1.3 - - '@types/react@19.1.8': + '@types/react@19.1.13': dependencies: csstype: 3.1.3 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/retry@0.12.0': {} @@ -16753,14 +16188,14 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 - '@types/semver@7.7.0': {} + '@types/semver@7.7.1': {} '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/serve-index@1.9.4': dependencies: @@ -16769,32 +16204,32 @@ snapshots: '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/send': 0.17.5 '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ssh2-streams@0.1.12': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ssh2@0.5.52': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ssh2-streams': 0.1.12 '@types/ssh2@1.15.5': dependencies: - '@types/node': 18.19.123 + '@types/node': 18.19.126 '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.17.2 - form-data: 4.0.3 + '@types/node': 22.18.5 + form-data: 4.0.4 '@types/supercluster@7.1.3': dependencies: @@ -16805,13 +16240,9 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 - '@types/tedious@4.0.14': - dependencies: - '@types/node': 22.17.2 - '@types/through@0.0.33': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/ua-parser-js@0.7.39': {} @@ -16819,13 +16250,15 @@ snapshots: '@types/unist@3.0.3': {} - '@types/validator@13.15.2': {} + '@types/uuid@9.0.8': {} + + '@types/validator@13.15.3': {} '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 '@types/yargs-parser@21.0.3': {} @@ -16833,32 +16266,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/type-utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.39.1 - eslint: 9.33.0(jiti@2.5.1) - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/eslint-plugin@8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/type-utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.39.1 - eslint: 9.33.0(jiti@2.5.1) + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.43.0 + eslint: 9.35.0(jiti@2.5.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -16867,157 +16283,57 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': + '@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.39.1 - debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.39.1 - debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.3 + eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.35.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.43.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) - '@typescript-eslint/types': 8.35.0 - debug: 4.4.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.39.1(typescript@5.8.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.8.3) - '@typescript-eslint/types': 8.39.1 - debug: 4.4.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.39.1(typescript@5.9.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.9.2) - '@typescript-eslint/types': 8.39.1 - debug: 4.4.1 + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) + '@typescript-eslint/types': 8.43.0 + debug: 4.4.3 typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.35.0': + '@typescript-eslint/scope-manager@8.43.0': dependencies: - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 - '@typescript-eslint/scope-manager@8.39.1': - dependencies: - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/visitor-keys': 8.39.1 - - '@typescript-eslint/tsconfig-utils@8.35.0(typescript@5.8.3)': - dependencies: - typescript: 5.8.3 - - '@typescript-eslint/tsconfig-utils@8.39.1(typescript@5.8.3)': - dependencies: - typescript: 5.8.3 - - '@typescript-eslint/tsconfig-utils@8.39.1(typescript@5.9.2)': + '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.35.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': - dependencies: - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': - dependencies: - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + debug: 4.4.3 + eslint: 9.35.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.35.0': {} + '@typescript-eslint/types@8.43.0': {} - '@typescript-eslint/types@8.39.1': {} - - '@typescript-eslint/typescript-estree@8.35.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.35.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.39.1(typescript@5.8.3)': - dependencies: - '@typescript-eslint/project-service': 8.39.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.8.3) - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/visitor-keys': 8.39.1 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.39.1(typescript@5.9.2)': - dependencies: - '@typescript-eslint/project-service': 8.39.1(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.9.2) - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/visitor-keys': 8.39.1 - debug: 4.4.1 + '@typescript-eslint/project-service': 8.43.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -17027,105 +16343,59 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': + '@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) - '@typescript-eslint/scope-manager': 8.35.0 - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - eslint: 9.33.0(jiti@2.5.1) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) - '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.8.3) - eslint: 9.33.0(jiti@2.5.1) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) - '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/types': 8.39.1 - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) - eslint: 9.33.0(jiti@2.5.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.35.0': + '@typescript-eslint/visitor-keys@8.43.0': dependencies: - '@typescript-eslint/types': 8.35.0 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.39.1': - dependencies: - '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.17 + magic-string: 0.30.19 magicast: 0.3.5 std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.17 + magic-string: 0.30.19 magicast: 0.3.5 std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.9.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -17137,29 +16407,21 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.19 optionalDependencies: - vite: 7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - '@vitest/mocker@3.2.4(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.19 optionalDependencies: - vite: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - - '@vitest/mocker@3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -17174,7 +16436,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 + magic-string: 0.30.19 pathe: 2.0.3 '@vitest/spy@3.2.4': @@ -17271,10 +16533,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.4(svelte@5.35.5)': + '@zoom-image/svelte@0.3.4(svelte@5.38.10)': dependencies: '@zoom-image/core': 0.41.0 - svelte: 5.35.5 + svelte: 5.38.10 abab@2.0.6: optional: true @@ -17325,7 +16587,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -17336,14 +16598,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-draft-04@1.0.0(ajv@8.11.0): - optionalDependencies: - ajv: 8.11.0 - - ajv-formats@2.1.1(ajv@8.11.0): - optionalDependencies: - ajv: 8.11.0 - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -17368,13 +16622,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.11.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -17417,7 +16664,7 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.2.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: @@ -17425,9 +16672,7 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} - - ansis@3.17.0: {} + ansi-styles@6.2.3: {} ansis@4.1.0: {} @@ -17438,8 +16683,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - app-root-path@3.1.0: {} - append-field@1.0.0: {} aproba@2.0.0: {} @@ -17505,7 +16748,7 @@ snapshots: ast-v8-to-istanbul@0.3.3: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.30 estree-walker: 3.0.3 js-tokens: 9.0.1 @@ -17519,10 +16762,6 @@ snapshots: async@0.2.10: {} - async@3.2.2: {} - - async@3.2.4: {} - async@3.2.6: {} asynckit@0.4.0: {} @@ -17533,8 +16772,8 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.1 - caniuse-lite: 1.0.30001726 + browserslist: 4.25.3 + caniuse-lite: 1.0.30001735 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -17545,12 +16784,12 @@ snapshots: b4a@1.6.7: {} - babel-loader@9.2.1(@babel/core@7.27.7)(webpack@5.99.9): + babel-loader@9.2.1(@babel/core@7.27.7)(webpack@5.100.2): dependencies: '@babel/core': 7.27.7 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.99.9 + webpack: 5.100.2 babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -17569,7 +16808,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.27.7) - core-js-compat: 3.43.0 + core-js-compat: 3.45.0 transitivePeerDependencies: - supports-color @@ -17586,9 +16825,6 @@ snapshots: balanced-match@1.0.2: {} - bare-events@2.5.4: - optional: true - bare-events@2.6.1: optional: true @@ -17635,19 +16871,17 @@ snapshots: big.js@5.2.2: {} - bignumber.js@9.3.1: {} - binary-extensions@2.3.0: {} - bits-ui@2.9.4(@internationalized/date@3.8.2)(svelte@5.35.5): + bits-ui@2.9.8(@internationalized/date@3.8.2)(svelte@5.38.10): dependencies: '@floating-ui/core': 1.7.3 - '@floating-ui/dom': 1.7.3 + '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.8.2 esm-env: 1.2.2 - runed: 0.29.2(svelte@5.35.5) - svelte: 5.35.5 - svelte-toolbelt: 0.9.3(svelte@5.35.5) + runed: 0.29.2(svelte@5.38.10) + svelte: 5.38.10 + svelte-toolbelt: 0.9.3(svelte@5.38.10) tabbable: 6.2.0 bl@4.1.0: @@ -17677,12 +16911,12 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 + debug: 4.4.3 http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 qs: 6.14.0 - raw-body: 3.0.0 + raw-body: 3.0.1 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -17694,6 +16928,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.12.1: {} + boxen@6.2.1: dependencies: ansi-align: 3.0.1 @@ -17709,7 +16945,7 @@ snapshots: dependencies: ansi-align: 3.0.1 camelcase: 7.0.1 - chalk: 5.6.0 + chalk: 5.6.2 cli-boxes: 3.0.0 string-width: 5.1.2 type-fest: 2.19.0 @@ -17729,13 +16965,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.1: - dependencies: - caniuse-lite: 1.0.30001726 - electron-to-chromium: 1.5.177 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) - browserslist@4.25.3: dependencies: caniuse-lite: 1.0.30001735 @@ -17762,7 +16991,7 @@ snapshots: builtin-modules@5.0.0: {} - bullmq@5.57.0: + bullmq@5.58.5: dependencies: cron-parser: 4.9.0 ioredis: 5.7.0 @@ -17832,8 +17061,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - call-me-maybe@1.0.2: {} - callsites@3.1.0: {} camel-case@4.1.2: @@ -17856,14 +17083,12 @@ snapshots: lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001726: {} - caniuse-lite@1.0.30001735: {} canvas@2.11.2: dependencies: '@mapbox/node-pre-gyp': 1.0.11 - nan: 2.22.2 + nan: 2.23.0 simple-get: 3.1.1 transitivePeerDependencies: - encoding @@ -17873,11 +17098,12 @@ snapshots: canvas@2.11.2(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) - nan: 2.22.2 + nan: 2.23.0 simple-get: 3.1.1 transitivePeerDependencies: - encoding - supports-color + optional: true ccount@2.0.1: {} @@ -17894,7 +17120,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.0: {} + chalk@5.6.2: {} change-case@5.4.4: {} @@ -17908,12 +17134,8 @@ snapshots: character-reference-invalid@2.0.1: {} - chardet@0.7.0: {} - chardet@2.1.0: {} - charset@1.0.1: {} - check-error@2.1.1: {} cheerio-select@2.1.0: @@ -17973,12 +17195,10 @@ snapshots: class-validator@0.14.2: dependencies: - '@types/validator': 13.15.2 + '@types/validator': 13.15.3 libphonenumber-js: 1.12.9 validator: 13.15.15 - classnames@2.5.1: {} - clean-css@5.3.3: dependencies: source-map: 0.6.1 @@ -18043,8 +17263,6 @@ snapshots: clone@1.0.4: {} - clsx@1.2.1: {} - clsx@2.1.1: {} cluster-key-slot@1.1.2: {} @@ -18137,19 +17355,6 @@ snapshots: transitivePeerDependencies: - supports-color - compute-gcd@1.2.1: - dependencies: - validate.io-array: 1.0.6 - validate.io-function: 1.0.2 - validate.io-integer-array: 1.0.0 - - compute-lcm@1.1.2: - dependencies: - compute-gcd: 1.2.1 - validate.io-array: 1.0.6 - validate.io-function: 1.0.2 - validate.io-integer-array: 1.0.0 - concat-map@0.0.1: {} concat-stream@2.0.0: @@ -18220,7 +17425,7 @@ snapshots: copy-text-to-clipboard@3.2.0: {} - copy-webpack-plugin@11.0.0(webpack@5.99.9): + copy-webpack-plugin@11.0.0(webpack@5.100.2): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -18228,15 +17433,11 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.99.9 - - core-js-compat@3.43.0: - dependencies: - browserslist: 4.25.3 + webpack: 5.100.2 core-js-compat@3.45.0: dependencies: - browserslist: 4.25.1 + browserslist: 4.25.3 core-js-pure@3.43.0: {} @@ -18282,7 +17483,7 @@ snapshots: cron-parser@4.9.0: dependencies: - luxon: 3.7.1 + luxon: 3.7.2 cron@4.3.0: dependencies: @@ -18295,8 +17496,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypto-js@4.2.0: {} - crypto-random-string@4.0.0: dependencies: type-fest: 1.4.0 @@ -18317,7 +17516,7 @@ snapshots: postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.99.9): + css-loader@6.11.0(webpack@5.100.2): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -18328,9 +17527,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.100.2 - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.99.9): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.100.2): dependencies: '@jridgewell/trace-mapping': 0.3.30 cssnano: 6.1.2(postcss@8.5.6) @@ -18338,7 +17537,7 @@ snapshots: postcss: 8.5.6 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.99.9 + webpack: 5.100.2 optionalDependencies: clean-css: 5.3.3 @@ -18488,8 +17687,6 @@ snapshots: whatwg-url: 14.2.0 optional: true - dayjs@1.11.13: {} - debounce@1.2.1: {} debounce@2.2.0: {} @@ -18502,16 +17699,13 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 decamelize@1.2.0: {} - decimal.js@10.5.0: {} - - decimal.js@10.6.0: - optional: true + decimal.js@10.6.0: {} decode-named-character-reference@1.2.0: dependencies: @@ -18520,13 +17714,12 @@ snapshots: decompress-response@4.2.1: dependencies: mimic-response: 2.1.0 + optional: true decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - dedent@1.6.0: {} - deep-eql@5.0.2: {} deep-equal@1.0.1: {} @@ -18577,22 +17770,18 @@ snapshots: detect-europe-js@0.1.2: {} - detect-libc@2.0.4: {} + detect-libc@2.1.0: {} detect-node@2.1.0: {} - detect-package-manager@3.0.2: - dependencies: - execa: 5.1.1 - detect-port@1.6.1: dependencies: address: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color - devalue@5.1.1: {} + devalue@5.3.2: {} devlop@1.1.0: dependencies: @@ -18629,7 +17818,7 @@ snapshots: docker-modem@5.0.6: dependencies: - debug: 4.4.1 + debug: 4.4.3 readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.16.0 @@ -18648,9 +17837,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -18668,129 +17857,6 @@ snapshots: unified: 9.2.2 unist-util-is: 4.1.0 - docusaurus-plugin-openapi@0.7.6(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2): - dependencies: - '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - chalk: 4.1.2 - clsx: 1.2.1 - js-yaml: 4.1.0 - json-refs: 3.0.15 - json-schema-resolve-allof: 1.5.0 - lodash: 4.17.21 - openapi-to-postmanv2: 4.25.0(encoding@0.1.13) - postman-collection: 4.5.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - webpack: 5.99.9 - transitivePeerDependencies: - - '@docusaurus/faster' - - '@mdx-js/react' - - '@parcel/css' - - '@rspack/core' - - '@swc/core' - - '@swc/css' - - acorn - - bufferutil - - csso - - debug - - encoding - - esbuild - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - webpack-cli - - docusaurus-plugin-proxy@0.7.6: {} - - docusaurus-preset-openapi@0.7.6(@algolia/client-search@5.29.0)(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)(search-insights@2.17.3)(typescript@5.9.2): - dependencies: - '@docusaurus/preset-classic': 3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(@types/react@19.1.10)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) - docusaurus-plugin-openapi: 0.7.6(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - docusaurus-plugin-proxy: 0.7.6 - docusaurus-theme-openapi: 0.7.6(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@types/react@19.1.10)(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)(typescript@5.9.2) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - transitivePeerDependencies: - - '@algolia/client-search' - - '@docusaurus/faster' - - '@docusaurus/plugin-content-docs' - - '@mdx-js/react' - - '@parcel/css' - - '@rspack/core' - - '@swc/core' - - '@swc/css' - - '@types/react' - - acorn - - bufferutil - - csso - - debug - - encoding - - esbuild - - lightningcss - - react-native - - redux - - search-insights - - supports-color - - typescript - - uglify-js - - utf-8-validate - - webpack-cli - - docusaurus-theme-openapi@0.7.6(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@types/react@19.1.10)(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)(typescript@5.9.2): - dependencies: - '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.0(@types/react@19.1.10)(react@18.3.1) - '@monaco-editor/react': 4.7.0(monaco-editor@0.31.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reduxjs/toolkit': 1.9.7(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - buffer: 6.0.3 - clsx: 1.2.1 - crypto-js: 4.2.0 - docusaurus-plugin-openapi: 0.7.6(@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1))(acorn@8.15.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - immer: 9.0.21 - lodash: 4.17.21 - marked: 11.2.0 - monaco-editor: 0.31.1 - postman-code-generators: 1.14.2 - postman-collection: 4.5.0 - prism-react-renderer: 2.4.1(react@18.3.1) - process: 0.11.10 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-magic-dropzone: 1.0.1 - react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - redux-devtools-extension: 2.13.9(redux@4.2.1) - refractor: 4.9.0 - striptags: 3.2.0 - webpack: 5.99.9 - transitivePeerDependencies: - - '@docusaurus/faster' - - '@docusaurus/plugin-content-docs' - - '@parcel/css' - - '@rspack/core' - - '@swc/core' - - '@swc/css' - - '@types/react' - - acorn - - bufferutil - - csso - - debug - - encoding - - esbuild - - lightningcss - - react-native - - redux - - supports-color - - typescript - - uglify-js - - utf-8-validate - - webpack-cli - dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -18849,9 +17915,7 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv@16.6.1: {} - - dotenv@17.2.1: {} + dotenv@17.2.2: {} dunder-proto@1.0.1: dependencies: @@ -18869,11 +17933,9 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.177: {} - electron-to-chromium@1.5.207: {} - emoji-regex@10.4.0: {} + emoji-regex@10.5.0: {} emoji-regex@8.0.0: {} @@ -18915,7 +17977,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 22.17.2 + '@types/node': 22.18.5 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -18928,11 +17990,6 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.18.2: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.2 - enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -18948,7 +18005,7 @@ snapshots: err-code@2.0.3: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -18982,8 +18039,6 @@ snapshots: es5-ext: 0.10.64 es6-symbol: 3.1.4 - es6-promise@3.3.1: {} - es6-symbol@3.1.4: dependencies: d: 1.0.2 @@ -19086,70 +18141,70 @@ snapshots: source-map: 0.6.1 optional: true - eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.5.1)): + eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.5.1)): dependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.35.0(jiti@2.5.1) - eslint-p@0.25.0(jiti@2.5.1): + eslint-p@0.26.0(jiti@2.5.1): dependencies: - eslint: 9.30.1(jiti@2.5.1) + eslint: 9.35.0(jiti@2.5.1) transitivePeerDependencies: - jiti - supports-color - eslint-plugin-compat@6.0.2(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-compat@6.0.2(eslint@9.35.0(jiti@2.5.1)): dependencies: '@mdn/browser-compat-data': 5.7.6 ast-metadata-inferer: 0.8.1 - browserslist: 4.25.1 - caniuse-lite: 1.0.30001726 - eslint: 9.33.0(jiti@2.5.1) + browserslist: 4.25.3 + caniuse-lite: 1.0.30001735 + eslint: 9.35.0(jiti@2.5.1) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 semver: 7.7.2 - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1))(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(prettier@3.6.2): dependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.35.0(jiti@2.5.1) prettier: 3.6.2 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.33.0(jiti@2.5.1)) + eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-svelte@3.11.0(eslint@9.33.0(jiti@2.5.1))(svelte@5.35.5): + eslint-plugin-svelte@3.12.3(eslint@9.35.0(jiti@2.5.1))(svelte@5.38.10): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.35.0(jiti@2.5.1) esutils: 2.0.3 - globals: 16.3.0 + globals: 16.4.0 known-css-properties: 0.37.0 postcss: 8.5.6 postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.2 - svelte-eslint-parser: 1.3.1(svelte@5.35.5) + svelte-eslint-parser: 1.3.2(svelte@5.38.10) optionalDependencies: - svelte: 5.35.5 + svelte: 5.38.10 transitivePeerDependencies: - ts-node - eslint-plugin-unicorn@60.0.0(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-unicorn@60.0.0(eslint@9.35.0(jiti@2.5.1)): dependencies: '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) - '@eslint/plugin-kit': 0.3.3 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1)) + '@eslint/plugin-kit': 0.3.5 change-case: 5.4.4 ci-info: 4.3.0 clean-regexp: 1.0.0 core-js-compat: 3.45.0 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.35.0(jiti@2.5.1) esquery: 1.6.0 find-up-simple: 1.0.1 - globals: 16.3.0 + globals: 16.4.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 @@ -19173,59 +18228,17 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.30.1(jiti@2.5.1): + eslint@9.35.0(jiti@2.5.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.5.1)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.30.1 - '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.1 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.5.1 - transitivePeerDependencies: - - supports-color - - eslint@9.33.0(jiti@2.5.1): - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 '@eslint/core': 0.15.2 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.33.0 + '@eslint/js': 9.35.0 '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.6 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -19233,7 +18246,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -19339,7 +18352,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 require-like: 0.1.2 event-emitter@0.3.5: @@ -19349,8 +18362,6 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter2@6.4.9: {} - eventemitter3@4.0.7: {} events@3.3.0: {} @@ -19379,7 +18390,7 @@ snapshots: batch-cluster: 13.0.0 exiftool-vendored.pl: 13.0.1 he: 1.2.0 - luxon: 3.7.1 + luxon: 3.7.2 optionalDependencies: exiftool-vendored.exe: 13.0.0 @@ -19431,7 +18442,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -19467,12 +18478,6 @@ snapshots: extend@3.0.2: {} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - fabric@6.7.1: optionalDependencies: canvas: 2.11.2 @@ -19512,6 +18517,10 @@ snapshots: fast-uri@3.0.6: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -19542,11 +18551,11 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.99.9): + file-loader@6.2.0(webpack@5.100.2): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.99.9 + webpack: 5.100.2 file-source@0.6.1: dependencies: @@ -19561,8 +18570,6 @@ snapshots: transitivePeerDependencies: - supports-color - file-type@3.9.0: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -19581,7 +18588,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -19628,14 +18635,12 @@ snapshots: follow-redirects@1.15.9: {} - foreach@2.0.6: {} - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -19650,18 +18655,10 @@ snapshots: semver: 7.7.2 tapable: 2.2.2 typescript: 5.8.3 - webpack: 5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17)) + webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) form-data-encoder@2.1.4: {} - form-data@4.0.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -19672,13 +18669,6 @@ snapshots: format@0.2.2: {} - formidable@2.1.5: - dependencies: - '@paralleldrive/cuid2': 2.2.2 - dezalgo: 1.0.4 - once: 1.4.0 - qs: 6.14.0 - formidable@3.5.4: dependencies: '@paralleldrive/cuid2': 2.2.2 @@ -19706,7 +18696,7 @@ snapshots: fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 fs-minipass@2.1.0: @@ -19741,26 +18731,6 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 - gaxios@6.7.1(encoding@0.1.13): - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0(encoding@0.1.13) - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - gcp-metadata@6.1.1(encoding@0.1.13): - dependencies: - gaxios: 6.7.1(encoding@0.1.13) - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - gensync@1.0.0-beta.2: {} geo-coordinates-parser@1.7.4: {} @@ -19786,7 +18756,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} + get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: dependencies: @@ -19810,14 +18780,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stdin@5.0.1: {} - get-stream@6.0.1: {} github-slugger@1.5.0: {} - gl-matrix@3.4.3: {} - gl-matrix@3.4.4: {} glob-parent@5.1.2: @@ -19867,7 +18833,7 @@ snapshots: globals@15.15.0: {} - globals@16.3.0: {} + globals@16.4.0: {} globalyzer@0.1.0: {} @@ -19890,8 +18856,6 @@ snapshots: globrex@0.1.2: {} - google-logging-utils@0.0.2: {} - gopd@1.2.0: {} got@12.6.1: @@ -19914,10 +18878,6 @@ snapshots: graphemer@1.4.0: {} - graphlib@2.1.8: - dependencies: - lodash: 4.17.21 - gray-matter@4.0.3: dependencies: js-yaml: 3.14.1 @@ -19996,10 +18956,6 @@ snapshots: hast-util-parse-selector@2.2.5: {} - hast-util-parse-selector@3.1.1: - dependencies: - '@types/hast': 2.3.10 - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -20110,14 +19066,6 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 - hastscript@7.2.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 3.1.1 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -20130,7 +19078,7 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 @@ -20199,7 +19147,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(webpack@5.99.9): + html-webpack-plugin@5.6.3(webpack@5.100.2): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -20207,7 +19155,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.100.2 htmlparser2@6.1.0: dependencies: @@ -20261,7 +19209,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color optional: true @@ -20269,7 +19217,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -20293,10 +19241,6 @@ snapshots: transitivePeerDependencies: - debug - http-reasons@0.1.0: {} - - http2-client@1.3.5: {} - http2-wrapper@2.2.1: dependencies: quick-lru: 5.1.1 @@ -20305,14 +19249,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -20330,6 +19274,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -20346,8 +19294,6 @@ snapshots: immediate@3.3.0: {} - immer@9.0.21: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -20385,13 +19331,13 @@ snapshots: inline-style-parser@0.2.4: {} - inquirer@8.2.6: + inquirer@8.2.7(@types/node@22.18.5): dependencies: + '@inquirer/external-editor': 1.0.2(@types/node@22.18.5) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 cli-width: 3.0.0 - external-editor: 3.1.0 figures: 3.2.0 lodash: 4.17.21 mute-stream: 0.0.8 @@ -20402,11 +19348,11 @@ snapshots: strip-ansi: 6.0.1 through: 2.3.8 wrap-ansi: 6.2.0 + transitivePeerDependencies: + - '@types/node' internmap@2.0.3: {} - interpret@1.4.0: {} - intl-messageformat@10.7.16: dependencies: '@formatjs/ecma402-abstract': 2.3.4 @@ -20422,7 +19368,7 @@ snapshots: dependencies: '@ioredis/commands': 1.3.0 cluster-key-slot: 1.1.2 - debug: 4.4.1 + debug: 4.4.3 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -20565,8 +19511,8 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.1 + '@jridgewell/trace-mapping': 0.3.30 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -20591,7 +19537,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.17.2 + '@types/node': 22.18.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -20599,13 +19545,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -20626,7 +19572,7 @@ snapshots: jose@5.10.0: {} - jose@6.0.12: {} + jose@6.1.0: {} js-tokens@4.0.0: {} @@ -20657,7 +19603,7 @@ snapshots: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.21 + nwsapi: 2.2.22 parse5: 7.3.0 saxes: 6.0.0 symbol-tree: 3.2.4 @@ -20686,7 +19632,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.21 + nwsapi: 2.2.22 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -20716,7 +19662,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.21 + nwsapi: 2.2.22 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -20741,46 +19687,10 @@ snapshots: jsesc@3.1.0: {} - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} - json-pointer@0.6.2: - dependencies: - foreach: 2.0.6 - - json-refs@3.0.15: - dependencies: - commander: 4.1.1 - graphlib: 2.1.8 - js-yaml: 3.14.1 - lodash: 4.17.21 - native-promise-only: 0.8.1 - path-loader: 1.0.12 - slash: 3.0.0 - uri-js: 4.4.1 - transitivePeerDependencies: - - supports-color - - json-schema-compare@0.2.2: - dependencies: - lodash: 4.17.21 - - json-schema-merge-allof@0.8.1: - dependencies: - compute-lcm: 1.1.2 - json-schema-compare: 0.2.2 - lodash: 4.17.21 - - json-schema-resolve-allof@1.5.0: - dependencies: - get-stdin: 5.0.1 - lodash: 4.17.21 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -20793,12 +19703,6 @@ snapshots: jsonc-parser@3.3.1: {} - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -20915,7 +19819,7 @@ snapshots: lightningcss@1.30.1: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.0 optionalDependencies: lightningcss-darwin-arm64: 1.30.1 lightningcss-darwin-x64: 1.30.1 @@ -20934,8 +19838,6 @@ snapshots: lines-and-columns@1.2.4: {} - liquid-json@0.3.1: {} - load-esm@1.0.2: {} load-tsconfig@0.2.5: {} @@ -20987,13 +19889,13 @@ snapshots: log-symbols@6.0.0: dependencies: - chalk: 5.6.0 + chalk: 5.6.2 is-unicode-supported: 1.3.0 log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 - yoctocolors: 2.1.1 + yoctocolors: 2.1.2 long@5.3.2: {} @@ -21013,7 +19915,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.1.0: {} + lru-cache@11.2.1: {} lru-cache@5.1.1: dependencies: @@ -21029,18 +19931,22 @@ snapshots: luxon@3.6.1: {} - luxon@3.7.1: {} + luxon@3.7.2: {} lz-string@1.5.0: {} magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 magicast@0.3.5: dependencies: - '@babel/parser': 7.27.7 - '@babel/types': 7.27.7 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 source-map-js: 1.2.1 make-dir@3.1.0: @@ -21092,7 +19998,7 @@ snapshots: tinyqueue: 2.0.3 vt-pbf: 3.1.3 - maplibre-gl@5.6.2: + maplibre-gl@5.7.1: dependencies: '@mapbox/geojson-rewind': 0.5.2 '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -21108,7 +20014,7 @@ snapshots: '@types/supercluster': 7.1.3 earcut: 3.0.2 geojson-vt: 4.0.2 - gl-matrix: 3.4.3 + gl-matrix: 3.4.4 kdbush: 4.0.2 murmurhash-js: 1.0.0 pbf: 4.0.1 @@ -21127,8 +20033,6 @@ snapshots: markdown-table@3.0.4: {} - marked@11.2.0: {} - marked@7.0.4: {} math-intrinsics@1.1.0: {} @@ -21635,7 +20539,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -21665,10 +20569,6 @@ snapshots: mime-db@1.54.0: {} - mime-format@2.0.1: - dependencies: - charset: 1.0.1 - mime-types@2.1.18: dependencies: mime-db: 1.33.0 @@ -21689,7 +20589,8 @@ snapshots: mimic-function@5.0.1: {} - mimic-response@2.1.0: {} + mimic-response@2.1.0: + optional: true mimic-response@3.1.0: {} @@ -21697,11 +20598,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.2(webpack@5.99.9): + mini-css-extract-plugin@2.9.2(webpack@5.100.2): dependencies: schema-utils: 4.3.2 tapable: 2.2.2 - webpack: 5.99.9 + webpack: 5.100.2 minimalistic-assert@1.0.1: {} @@ -21784,8 +20685,6 @@ snapshots: module-details-from-path@1.0.4: {} - monaco-editor@0.31.1: {} - moo@0.5.2: {} mri@1.2.0: {} @@ -21839,8 +20738,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.22.2: {} - nan@2.23.0: optional: true @@ -21848,8 +20745,6 @@ snapshots: nanoid@5.1.5: {} - native-promise-only@0.8.1: {} - natural-compare@1.4.0: {} nearley@2.20.1: @@ -21867,9 +20762,7 @@ snapshots: neo-async@2.6.2: {} - neotraverse@0.6.15: {} - - nest-commander@3.18.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@types/inquirer@8.2.11)(typescript@5.9.2): + nest-commander@3.19.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@types/inquirer@8.2.11)(@types/node@22.18.5)(typescript@5.9.2): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) '@golevelup/nestjs-discovery': 4.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) @@ -21878,8 +20771,9 @@ snapshots: '@types/inquirer': 8.2.11 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.2) - inquirer: 8.2.6 + inquirer: 8.2.7(@types/node@22.18.5) transitivePeerDependencies: + - '@types/node' - typescript nestjs-cls@5.4.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2): @@ -21917,8 +20811,6 @@ snapshots: node-addon-api@4.3.0: {} - node-addon-api@8.4.0: {} - node-addon-api@8.5.0: {} node-emoji@1.11.0: @@ -21932,10 +20824,6 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 - node-fetch-h2@2.3.0: - dependencies: - http2-client: 1.3.5 - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -21951,12 +20839,12 @@ snapshots: node-gyp-build-optional-packages@5.2.2: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.0 optional: true node-gyp-build@4.8.4: {} - node-gyp@11.2.0: + node-gyp@11.4.2: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.2 @@ -21966,33 +20854,14 @@ snapshots: proc-log: 5.0.0 semver: 7.7.2 tar: 7.4.3 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 which: 5.0.0 transitivePeerDependencies: - supports-color - node-gyp@11.3.0: - dependencies: - env-paths: 2.2.1 - exponential-backoff: 3.1.2 - graceful-fs: 4.2.11 - make-fetch-happen: 14.0.3 - nopt: 8.1.0 - proc-log: 5.0.0 - semver: 7.7.2 - tar: 7.4.3 - tinyglobby: 0.2.14 - which: 5.0.0 - transitivePeerDependencies: - - supports-color - - node-readfiles@0.2.0: - dependencies: - es6-promise: 3.3.1 - node-releases@2.0.19: {} - nodemailer@7.0.5: {} + nodemailer@7.0.6: {} nopt@1.0.10: dependencies: @@ -22033,13 +20902,13 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.99.9): + null-loader@4.0.1(webpack@5.100.2): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.99.9 + webpack: 5.100.2 - nwsapi@2.2.21: + nwsapi@2.2.22: optional: true nypm@0.6.0: @@ -22047,50 +20916,10 @@ snapshots: citty: 0.1.6 consola: 3.4.2 pathe: 2.0.3 - pkg-types: 2.2.0 + pkg-types: 2.3.0 tinyexec: 0.3.2 - oas-kit-common@1.0.8: - dependencies: - fast-safe-stringify: 2.1.1 - - oas-linter@3.2.2: - dependencies: - '@exodus/schemasafe': 1.3.0 - should: 13.2.3 - yaml: 1.10.2 - - oas-resolver-browser@2.5.6: - dependencies: - node-fetch-h2: 2.3.0 - oas-kit-common: 1.0.8 - path-browserify: 1.0.1 - reftools: 1.1.9 - yaml: 1.10.2 - yargs: 17.7.2 - - oas-resolver@2.5.6: - dependencies: - node-fetch-h2: 2.3.0 - oas-kit-common: 1.0.8 - reftools: 1.1.9 - yaml: 1.10.2 - yargs: 17.7.2 - - oas-schema-walker@1.1.5: {} - - oas-validator@5.0.8: - dependencies: - call-me-maybe: 1.0.2 - oas-kit-common: 1.0.8 - oas-linter: 3.2.2 - oas-resolver: 2.5.6 - oas-schema-walker: 1.1.5 - reftools: 1.1.9 - should: 13.2.3 - yaml: 1.10.2 - - oauth4webapi@3.7.0: {} + oauth4webapi@3.8.1: {} object-assign@4.1.1: {} @@ -22113,18 +20942,18 @@ snapshots: obuf@1.1.2: {} - oidc-provider@9.4.1: + oidc-provider@9.5.1: dependencies: '@koa/cors': 5.0.0 '@koa/router': 14.0.0 - debug: 4.4.1 + debug: 4.4.3 eta: 3.5.0 - jose: 6.0.12 + jose: 6.1.0 jsesc: 3.1.0 koa: 3.0.1 nanoid: 5.1.5 - quick-lru: 7.0.1 - raw-body: 3.0.0 + quick-lru: 7.2.0 + raw-body: 3.0.1 transitivePeerDependencies: - supports-color @@ -22152,34 +20981,12 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openapi-to-postmanv2@4.25.0(encoding@0.1.13): - dependencies: - ajv: 8.11.0 - ajv-draft-04: 1.0.0(ajv@8.11.0) - ajv-formats: 2.1.1(ajv@8.11.0) - async: 3.2.4 - commander: 2.20.3 - graphlib: 2.1.8 - js-yaml: 4.1.0 - json-pointer: 0.6.2 - json-schema-merge-allof: 0.8.1 - lodash: 4.17.21 - neotraverse: 0.6.15 - oas-resolver-browser: 2.5.6 - object-hash: 3.0.0 - path-browserify: 1.0.1 - postman-collection: 4.5.0 - swagger2openapi: 7.0.8(encoding@0.1.13) - yaml: 1.10.2 - transitivePeerDependencies: - - encoding - opener@1.5.2: {} - openid-client@6.6.4: + openid-client@6.8.0: dependencies: - jose: 6.0.12 - oauth4webapi: 3.7.0 + jose: 6.1.0 + oauth4webapi: 3.8.1 optionator@0.9.4: dependencies: @@ -22204,7 +21011,7 @@ snapshots: ora@8.2.0: dependencies: - chalk: 5.6.0 + chalk: 5.6.2 cli-cursor: 5.0.0 cli-spinners: 2.9.2 is-interactive: 2.0.0 @@ -22212,9 +21019,7 @@ snapshots: log-symbols: 6.0.0 stdin-discarder: 0.2.2 string-width: 7.2.0 - strip-ansi: 7.1.0 - - os-tmpdir@1.0.2: {} + strip-ansi: 7.1.2 p-cancelable@3.0.0: {} @@ -22297,7 +21102,7 @@ snapshots: parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -22328,8 +21133,6 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 - path-browserify@1.0.1: {} - path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -22340,13 +21143,6 @@ snapshots: path-key@3.1.1: {} - path-loader@1.0.12: - dependencies: - native-promise-only: 0.8.1 - superagent: 7.1.6 - transitivePeerDependencies: - - supports-color - path-parse@1.0.7: {} path-scurry@1.11.1: @@ -22356,7 +21152,7 @@ snapshots: path-scurry@2.0.0: dependencies: - lru-cache: 11.1.0 + lru-cache: 11.2.1 minipass: 7.1.2 path-source@0.1.3: @@ -22374,12 +21170,9 @@ snapshots: path-to-regexp@8.2.0: {} - path-type@4.0.0: {} + path-to-regexp@8.3.0: {} - path@0.12.7: - dependencies: - process: 0.11.10 - util: 0.10.4 + path-type@4.0.0: {} pathe@2.0.3: {} @@ -22447,17 +21240,17 @@ snapshots: dependencies: find-up: 6.3.0 - pkg-types@2.2.0: + pkg-types@2.3.0: dependencies: confbox: 0.2.2 exsolve: 1.0.7 pathe: 2.0.3 - playwright-core@1.54.2: {} + playwright-core@1.55.0: {} - playwright@1.54.2: + playwright@1.55.0: dependencies: - playwright-core: 1.54.2 + playwright-core: 1.55.0 optionalDependencies: fsevents: 2.3.2 @@ -22465,7 +21258,7 @@ snapshots: pmtiles@3.2.1: dependencies: - '@types/leaflet': 1.9.19 + '@types/leaflet': 1.9.20 fflate: 0.8.2 pmtiles@4.3.0: @@ -22498,7 +21291,7 @@ snapshots: postcss-color-functional-notation@7.0.10(postcss@8.5.6): dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -22620,14 +21413,14 @@ snapshots: read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.6): + postcss-js@4.1.0(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 postcss: 8.5.6 postcss-lab-function@7.0.10(postcss@8.5.6): dependencies: - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) @@ -22648,13 +21441,13 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.2)(webpack@5.99.9): + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.2)(webpack@5.100.2): dependencies: cosmiconfig: 8.3.6(typescript@5.9.2) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.2 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - typescript @@ -22845,7 +21638,7 @@ snapshots: '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6) '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6) autoprefixer: 10.4.21(postcss@8.5.6) - browserslist: 4.25.1 + browserslist: 4.25.3 css-blank-pseudo: 7.0.1(postcss@8.5.6) css-has-pseudo: 7.0.2(postcss@8.5.6) css-prefers-color-scheme: 10.0.0(postcss@8.5.6) @@ -22965,33 +21758,6 @@ snapshots: postgres@3.4.7: {} - postman-code-generators@1.14.2: - dependencies: - async: 3.2.2 - detect-package-manager: 3.0.2 - lodash: 4.17.21 - path: 0.12.7 - postman-collection: 4.5.0 - shelljs: 0.8.5 - - postman-collection@4.5.0: - dependencies: - '@faker-js/faker': 5.5.3 - file-type: 3.9.0 - http-reasons: 0.1.0 - iconv-lite: 0.6.3 - liquid-json: 0.3.1 - lodash: 4.17.21 - mime-format: 2.0.1 - mime-types: 2.1.35 - postman-url-encoder: 3.0.5 - semver: 7.6.3 - uuid: 8.3.2 - - postman-url-encoder@3.0.5: - dependencies: - punycode: 2.3.1 - potpack@1.0.2: {} potpack@2.1.0: {} @@ -23002,11 +21768,6 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.8.3): - dependencies: - prettier: 3.6.2 - typescript: 5.8.3 - prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2): dependencies: prettier: 3.6.2 @@ -23016,10 +21777,10 @@ snapshots: dependencies: prettier: 3.6.2 - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.35.5): + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.38.10): dependencies: prettier: 3.6.2 - svelte: 5.35.5 + svelte: 5.38.10 prettier@3.6.2: {} @@ -23098,7 +21859,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.17.2 + '@types/node': 22.18.5 long: 5.3.2 protocol-buffers-schema@3.6.0: {} @@ -23147,7 +21908,7 @@ snapshots: quick-lru@5.1.1: {} - quick-lru@7.0.1: {} + quick-lru@7.2.0: {} quickselect@2.0.0: {} @@ -23175,18 +21936,18 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@3.0.0: + raw-body@3.0.1: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.7.0 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.99.9): + raw-loader@4.0.2(webpack@5.100.2): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.99.9 + webpack: 5.100.2 rc@1.2.8: dependencies: @@ -23206,11 +21967,10 @@ snapshots: react: 19.1.1 scheduler: 0.26.0 - react-email@4.2.8: + react-email@4.2.11: dependencies: - '@babel/parser': 7.28.3 - '@babel/traverse': 7.28.3 - chalk: 5.6.0 + '@babel/parser': 7.28.4 + '@babel/traverse': 7.28.4 chokidar: 4.0.3 commander: 13.1.0 debounce: 2.2.0 @@ -23240,39 +22000,25 @@ snapshots: dependencies: react: 18.3.1 - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.99.9): + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.100.2): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - webpack: 5.99.9 - - react-magic-dropzone@1.0.1: {} + webpack: 5.100.2 react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 - react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@babel/runtime': 7.28.3 - '@types/react-redux': 7.1.34 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 18.3.1 - react-is: 17.0.2 - optionalDependencies: - react-dom: 18.3.1(react@18.3.1) - react-router-config@5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 react: 18.3.1 react-router: 5.3.4(react@18.3.1) react-router-dom@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -23283,7 +22029,7 @@ snapshots: react-router@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -23338,10 +22084,6 @@ snapshots: readdirp@4.1.2: {} - rechoir@0.6.2: - dependencies: - resolve: 1.22.10 - recma-build-jsx@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -23383,29 +22125,8 @@ snapshots: dependencies: redis-errors: 1.2.0 - redux-devtools-extension@2.13.9(redux@4.2.1): - dependencies: - redux: 4.2.1 - - redux-thunk@2.4.2(redux@4.2.1): - dependencies: - redux: 4.2.1 - - redux@4.2.1: - dependencies: - '@babel/runtime': 7.28.3 - reflect-metadata@0.2.2: {} - refractor@4.9.0: - dependencies: - '@types/hast': 2.3.10 - '@types/prismjs': 1.26.5 - hastscript: 7.2.0 - parse-entities: 4.0.2 - - reftools@1.1.9: {} - regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 @@ -23541,7 +22262,7 @@ snapshots: require-in-the-middle@7.5.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 module-details-from-path: 1.0.4 resolve: 1.22.10 transitivePeerDependencies: @@ -23553,8 +22274,6 @@ snapshots: requires-port@1.0.0: {} - reselect@4.1.8: {} - resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -23602,55 +22321,51 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.0.1: - dependencies: - glob: 11.0.3 - package-json-from-dist: 1.0.1 - robust-predicates@3.0.2: {} - rollup-plugin-visualizer@6.0.3(rollup@4.46.3): + rollup-plugin-visualizer@6.0.3(rollup@4.50.1): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: - rollup: 4.46.3 + rollup: 4.50.1 - rollup@4.46.3: + rollup@4.50.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.3 - '@rollup/rollup-android-arm64': 4.46.3 - '@rollup/rollup-darwin-arm64': 4.46.3 - '@rollup/rollup-darwin-x64': 4.46.3 - '@rollup/rollup-freebsd-arm64': 4.46.3 - '@rollup/rollup-freebsd-x64': 4.46.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.3 - '@rollup/rollup-linux-arm-musleabihf': 4.46.3 - '@rollup/rollup-linux-arm64-gnu': 4.46.3 - '@rollup/rollup-linux-arm64-musl': 4.46.3 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.3 - '@rollup/rollup-linux-ppc64-gnu': 4.46.3 - '@rollup/rollup-linux-riscv64-gnu': 4.46.3 - '@rollup/rollup-linux-riscv64-musl': 4.46.3 - '@rollup/rollup-linux-s390x-gnu': 4.46.3 - '@rollup/rollup-linux-x64-gnu': 4.46.3 - '@rollup/rollup-linux-x64-musl': 4.46.3 - '@rollup/rollup-win32-arm64-msvc': 4.46.3 - '@rollup/rollup-win32-ia32-msvc': 4.46.3 - '@rollup/rollup-win32-x64-msvc': 4.46.3 + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 fsevents: 2.3.3 router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 transitivePeerDependencies: - supports-color @@ -23670,10 +22385,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.29.2(svelte@5.35.5): + runed@0.29.2(svelte@5.38.10): dependencies: esm-env: 1.2.2 - svelte: 5.35.5 + svelte: 5.38.10 rw@1.3.3: {} @@ -23760,8 +22475,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.3: {} - semver@7.7.2: {} send@0.19.0: @@ -23784,7 +22497,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -23859,11 +22572,6 @@ snapshots: setprototypeof@1.2.0: {} - sha.js@2.4.11: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - shallow-clone@3.0.1: dependencies: kind-of: 6.0.3 @@ -23879,35 +22587,36 @@ snapshots: stream-source: 0.3.5 text-encoding: 0.6.4 - sharp@0.34.2: + sharp@0.34.3: dependencies: color: 4.2.3 - detect-libc: 2.0.4 - node-addon-api: 8.4.0 - node-gyp: 11.2.0 + detect-libc: 2.1.0 + node-addon-api: 8.5.0 + node-gyp: 11.4.2 semver: 7.7.2 optionalDependencies: - '@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 - '@img/sharp-libvips-linux-arm64': 1.1.0 - '@img/sharp-libvips-linux-ppc64': 1.1.0 - '@img/sharp-libvips-linux-s390x': 1.1.0 - '@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.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 + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 transitivePeerDependencies: - supports-color @@ -23919,38 +22628,6 @@ snapshots: shell-quote@1.8.3: {} - shelljs@0.8.5: - dependencies: - glob: 7.2.3 - interpret: 1.4.0 - rechoir: 0.6.2 - - should-equal@2.0.0: - dependencies: - should-type: 1.4.0 - - should-format@3.0.3: - dependencies: - should-type: 1.4.0 - should-type-adaptors: 1.1.0 - - should-type-adaptors@1.1.0: - dependencies: - should-type: 1.4.0 - should-util: 1.0.1 - - should-type@1.4.0: {} - - should-util@1.0.1: {} - - should@13.2.3: - dependencies: - should-equal: 2.0.0 - should-format: 3.0.3 - should-type: 1.4.0 - should-type-adaptors: 1.1.0 - should-util: 1.0.1 - side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -23985,13 +22662,17 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} + simple-concat@1.0.1: + optional: true simple-get@3.1.1: dependencies: decompress-response: 4.2.1 once: 1.4.0 simple-concat: 1.0.1 + optional: true + + simple-icons@15.15.0: {} simple-swizzle@0.2.2: dependencies: @@ -24003,7 +22684,7 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 - sirv@3.0.1: + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 @@ -24085,7 +22766,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -24114,7 +22795,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -24125,7 +22806,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -24139,13 +22820,11 @@ snapshots: sprintf-js@1.0.3: {} - sql-formatter@15.6.6: + sql-formatter@15.6.9: dependencies: argparse: 2.0.1 nearley: 2.20.1 - sql-highlight@6.1.0: {} - srcset@4.0.0: {} ssh-remote-port-forward@1.0.4: @@ -24169,8 +22848,6 @@ snapshots: standard-as-callback@2.1.0: {} - state-local@1.0.7: {} - statuses@1.5.0: {} statuses@2.0.1: {} @@ -24190,7 +22867,7 @@ snapshots: fast-fifo: 1.3.2 text-decoder: 1.2.3 optionalDependencies: - bare-events: 2.5.4 + bare-events: 2.6.1 string-width@4.2.3: dependencies: @@ -24202,13 +22879,13 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@7.2.0: dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 + emoji-regex: 10.5.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 string_decoder@1.1.1: dependencies: @@ -24233,9 +22910,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.2.0 + ansi-regex: 6.2.2 strip-bom-string@1.0.0: {} @@ -24259,7 +22936,7 @@ snapshots: dependencies: js-tokens: 9.0.1 - striptags@3.2.0: {} + strnum@2.1.1: {} strtok3@10.3.4: dependencies: @@ -24293,7 +22970,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.1 + debug: 4.4.3 fast-safe-stringify: 2.1.1 form-data: 4.0.4 formidable: 3.5.4 @@ -24303,22 +22980,6 @@ snapshots: transitivePeerDependencies: - supports-color - superagent@7.1.6: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.1 - fast-safe-stringify: 2.1.1 - form-data: 4.0.4 - formidable: 2.1.5 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.14.0 - readable-stream: 3.6.2 - semver: 7.7.2 - transitivePeerDependencies: - - supports-color - supercluster@7.1.5: dependencies: kdbush: 3.0.0 @@ -24344,19 +23005,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.1(picomatch@4.0.3)(svelte@5.35.5)(typescript@5.8.3): + svelte-check@4.3.1(picomatch@4.0.3)(svelte@5.38.10)(typescript@5.9.2): dependencies: '@jridgewell/trace-mapping': 0.3.30 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.35.5 - typescript: 5.8.3 + svelte: 5.38.10 + typescript: 5.9.2 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.3.1(svelte@5.35.5): + svelte-eslint-parser@1.3.2(svelte@5.38.10): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -24365,11 +23026,11 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.0 optionalDependencies: - svelte: 5.35.5 + svelte: 5.38.10 - svelte-gestures@5.1.4: {} + svelte-gestures@5.2.2: {} - svelte-i18n@4.0.1(svelte@5.35.5): + svelte-i18n@4.0.1(svelte@5.38.10): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -24377,36 +23038,36 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.16 sade: 1.8.1 - svelte: 5.35.5 + svelte: 5.38.10 tiny-glob: 0.2.9 - svelte-maplibre@1.2.0(svelte@5.35.5): + svelte-maplibre@1.2.1(svelte@5.38.10): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 - maplibre-gl: 5.6.2 + maplibre-gl: 5.7.1 pmtiles: 3.2.1 - svelte: 5.35.5 + svelte: 5.38.10 - svelte-parse-markup@0.1.5(svelte@5.35.5): + svelte-parse-markup@0.1.5(svelte@5.38.10): dependencies: - svelte: 5.35.5 + svelte: 5.38.10 - svelte-persisted-store@0.12.0(svelte@5.35.5): + svelte-persisted-store@0.12.0(svelte@5.38.10): dependencies: - svelte: 5.35.5 + svelte: 5.38.10 - svelte-toolbelt@0.9.3(svelte@5.35.5): + svelte-toolbelt@0.9.3(svelte@5.38.10): dependencies: clsx: 2.1.1 - runed: 0.29.2(svelte@5.35.5) + runed: 0.29.2(svelte@5.38.10) style-to-object: 1.0.9 - svelte: 5.35.5 + svelte: 5.38.10 - svelte@5.35.5: + svelte@5.38.10: dependencies: - '@ampproject/remapping': 2.3.0 + '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) '@types/estree': 1.0.8 @@ -24418,7 +23079,7 @@ snapshots: esrap: 2.1.0 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.17 + magic-string: 0.30.19 zimmerframe: 1.1.2 svg-parser@2.0.4: {} @@ -24437,22 +23098,6 @@ snapshots: dependencies: '@scarf/scarf': 1.4.0 - swagger2openapi@7.0.8(encoding@0.1.13): - dependencies: - call-me-maybe: 1.0.2 - node-fetch: 2.7.0(encoding@0.1.13) - node-fetch-h2: 2.3.0 - node-readfiles: 0.2.0 - oas-kit-common: 1.0.8 - oas-resolver: 2.5.6 - oas-schema-walker: 1.1.5 - oas-validator: 5.0.8 - reftools: 1.1.9 - yaml: 1.10.2 - yargs: 17.7.2 - transitivePeerDependencies: - - encoding - symbol-observable@4.0.0: {} symbol-tree@3.2.4: @@ -24468,9 +23113,9 @@ snapshots: tailwind-merge@3.3.1: {} - tailwind-variants@2.1.0(tailwind-merge@3.3.1)(tailwindcss@4.1.12): + tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.13): dependencies: - tailwindcss: 4.1.12 + tailwindcss: 4.1.13 optionalDependencies: tailwind-merge: 3.3.1 @@ -24506,7 +23151,7 @@ snapshots: picocolors: 1.1.1 postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.0.1(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) postcss-load-config: 4.0.2(postcss@8.5.6) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 @@ -24515,7 +23160,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@4.1.12: {} + tailwindcss@4.1.13: {} tapable@2.2.2: {} @@ -24568,25 +23213,25 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser-webpack-plugin@5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17))): + terser-webpack-plugin@5.3.14(@swc/core@1.13.5(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17)) + webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) optionalDependencies: - '@swc/core': 1.13.3(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.17) - terser-webpack-plugin@5.3.14(webpack@5.99.9): + terser-webpack-plugin@5.3.14(webpack@5.100.2): dependencies: '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.99.9 + webpack: 5.100.2 terser@5.43.1: dependencies: @@ -24608,7 +23253,7 @@ snapshots: archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 - debug: 4.4.1 + debug: 4.4.3 docker-compose: 1.2.0 dockerode: 4.0.7 get-port: 7.1.0 @@ -24617,7 +23262,7 @@ snapshots: ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.0 tmp: 0.2.5 - undici: 7.14.0 + undici: 7.16.0 transitivePeerDependencies: - bare-buffer - supports-color @@ -24636,10 +23281,10 @@ snapshots: dependencies: any-promise: 1.3.0 - three@0.175.0: {} - three@0.179.1: {} + three@0.180.0: {} + through@2.3.8: {} thumbhash@0.1.1: {} @@ -24664,7 +23309,7 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.14: + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 @@ -24687,10 +23332,6 @@ snapshots: tldts-core: 6.1.86 optional: true - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - tmp@0.2.5: {} to-regex-range@5.0.1: @@ -24749,10 +23390,6 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 - ts-api-utils@2.1.0(typescript@5.8.3): - dependencies: - typescript: 5.8.3 - ts-api-utils@2.1.0(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -24811,48 +23448,13 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.25(ioredis@5.7.0)(pg@8.16.3)(reflect-metadata@0.2.2): + typescript-eslint@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2): dependencies: - '@sqltools/formatter': 1.2.5 - ansis: 3.17.0 - app-root-path: 3.1.0 - buffer: 6.0.3 - dayjs: 1.11.13 - debug: 4.4.1 - dedent: 1.6.0 - dotenv: 16.6.1 - glob: 10.4.5 - reflect-metadata: 0.2.2 - sha.js: 2.4.11 - sql-highlight: 6.1.0 - tslib: 2.8.1 - uuid: 11.1.0 - yargs: 17.7.2 - optionalDependencies: - ioredis: 5.7.0 - pg: 8.16.3 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - typescript-eslint@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) - eslint: 9.33.0(jiti@2.5.1) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - typescript-eslint@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2): - dependencies: - '@typescript-eslint/eslint-plugin': 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.33.0(jiti@2.5.1) + '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -24863,15 +23465,12 @@ snapshots: ua-is-frozen@0.1.2: {} - ua-parser-js@2.0.4(encoding@0.1.13): + ua-parser-js@2.0.5: dependencies: - '@types/node-fetch': 2.6.12 detect-europe-js: 0.1.2 is-standalone-pwa: 0.1.1 - node-fetch: 2.7.0(encoding@0.1.13) ua-is-frozen: 0.1.2 - transitivePeerDependencies: - - encoding + undici: 7.16.0 uglify-js@3.19.3: optional: true @@ -24886,14 +23485,12 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.20.0: {} - undici-types@6.21.0: {} - undici-types@7.10.0: + undici-types@7.12.0: optional: true - undici@7.14.0: {} + undici@7.16.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -24995,27 +23592,22 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.5(@swc/core@1.13.3(@swc/helpers@0.5.17))(rollup@4.46.3): + unplugin-swc@1.5.7(@swc/core@1.13.5(@swc/helpers@0.5.17))(rollup@4.50.1): dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.3) - '@swc/core': 1.13.3(@swc/helpers@0.5.17) + '@rollup/pluginutils': 5.3.0(rollup@4.50.1) + '@swc/core': 1.13.5(@swc/helpers@0.5.17) load-tsconfig: 0.2.5 - unplugin: 2.3.5 + unplugin: 2.3.10 transitivePeerDependencies: - rollup - unplugin@2.3.5: + unplugin@2.3.10: dependencies: + '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.3(browserslist@4.25.1): - dependencies: - browserslist: 4.25.1 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.3): dependencies: browserslist: 4.25.3 @@ -25025,7 +23617,7 @@ snapshots: update-notifier@6.0.2: dependencies: boxen: 7.1.1 - chalk: 5.6.0 + chalk: 5.6.2 configstore: 6.0.0 has-yarn: 3.0.0 import-lazy: 4.0.0 @@ -25043,14 +23635,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.100.2))(webpack@5.100.2): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.99.9 + webpack: 5.100.2 optionalDependencies: - file-loader: 6.2.0(webpack@5.99.9) + file-loader: 6.2.0(webpack@5.100.2) url-parse@1.5.10: dependencies: @@ -25067,10 +23659,6 @@ snapshots: util-deprecate@1.0.2: {} - util@0.10.4: - dependencies: - inherits: 2.0.3 - utila@0.4.0: {} utility-types@3.11.0: {} @@ -25093,21 +23681,6 @@ snapshots: uuid@9.0.1: {} - validate.io-array@1.0.6: {} - - validate.io-function@1.0.2: {} - - validate.io-integer-array@1.0.0: - dependencies: - validate.io-array: 1.0.6 - validate.io-integer: 1.0.5 - - validate.io-integer@1.0.5: - dependencies: - validate.io-number: 1.0.3 - - validate.io-number@1.0.3: {} - validator@13.15.15: {} value-equal@1.0.1: {} @@ -25143,22 +23716,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-imagetools@8.0.0(rollup@4.46.3): + vite-imagetools@8.0.0(rollup@4.50.1): dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.3) + '@rollup/pluginutils': 5.3.0(rollup@4.50.1) imagetools-core: 8.0.0 - sharp: 0.34.2 + sharp: 0.34.3 transitivePeerDependencies: - rollup - supports-color - vite-node@3.2.4(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -25173,13 +23746,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -25194,133 +23767,85 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): - dependencies: - debug: 4.4.1 + debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: - vite: 7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): - dependencies: - debug: 4.4.1 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.2) - optionalDependencies: - vite: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - typescript - - vite@7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.3 - tinyglobby: 0.2.14 + rollup: 4.50.1 + tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.5 fsevents: 2.3.3 jiti: 2.5.1 lightningcss: 1.30.1 terser: 5.43.1 yaml: 2.8.1 - vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.3 - tinyglobby: 0.2.14 + rollup: 4.50.1 + tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.17.2 + '@types/node': 24.5.1 fsevents: 2.3.3 jiti: 2.5.1 lightningcss: 1.30.1 terser: 5.43.1 yaml: 2.8.1 - vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): - dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.46.3 - tinyglobby: 0.2.14 + vitefu@1.1.1(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): optionalDependencies: - '@types/node': 24.3.0 - fsevents: 2.3.3 - jiti: 2.5.1 - lightningcss: 1.30.1 - terser: 5.43.1 - yaml: 2.8.1 + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vitefu@1.1.1(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): - optionalDependencies: - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.1 + debug: 4.4.3 expect-type: 1.2.1 - magic-string: 0.30.17 + magic-string: 0.30.19 pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.2(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.13.14 + '@types/node': 22.18.5 happy-dom: 18.0.1 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: @@ -25337,36 +23862,36 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.5)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.1 + debug: 4.4.3 expect-type: 1.2.1 - magic-string: 0.30.17 + magic-string: 0.30.19 pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.5)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.17.2 + '@types/node': 22.18.5 happy-dom: 18.0.1 - jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) + jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti - less @@ -25381,34 +23906,34 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.1)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.1 + debug: 4.4.3 expect-type: 1.2.1 - magic-string: 0.30.17 + magic-string: 0.30.19 pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.3.0 + '@types/node': 24.5.1 happy-dom: 18.0.1 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: @@ -25481,16 +24006,16 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@5.3.4(webpack@5.99.9): + webpack-dev-middleware@5.3.4(webpack@5.100.2): dependencies: colorette: 2.0.20 memfs: 3.5.3 mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.3.2 - webpack: 5.99.9 + webpack: 5.100.2 - webpack-dev-server@4.15.2(webpack@5.99.9): + webpack-dev-server@4.15.2(webpack@5.100.2): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -25520,10 +24045,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 5.3.4(webpack@5.99.9) + webpack-dev-middleware: 5.3.4(webpack@5.100.2) ws: 8.18.3 optionalDependencies: - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - bufferutil - debug @@ -25548,7 +24073,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17)): + webpack@5.100.2: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -25572,7 +24097,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17))) + terser-webpack-plugin: 5.3.14(webpack@5.100.2) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -25580,7 +24105,7 @@ snapshots: - esbuild - uglify-js - webpack@5.99.9: + webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -25589,9 +24114,10 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.1 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.25.3 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.2 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -25603,7 +24129,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(webpack@5.99.9) + terser-webpack-plugin: 5.3.14(@swc/core@1.13.5(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -25611,7 +24137,7 @@ snapshots: - esbuild - uglify-js - webpackbar@6.0.1(webpack@5.99.9): + webpackbar@6.0.1(webpack@5.100.2): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -25620,7 +24146,7 @@ snapshots: markdown-table: 2.0.0 pretty-time: 1.1.0 std-env: 3.9.0 - webpack: 5.99.9 + webpack: 5.100.2 wrap-ansi: 7.0.0 websocket-driver@0.7.4: @@ -25710,9 +24236,9 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrappy@1.0.2: {} @@ -25799,7 +24325,7 @@ snapshots: yoctocolors-cjs@2.1.2: {} - yoctocolors@2.1.1: {} + yoctocolors@2.1.2: {} zimmerframe@1.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2114040be3..880d115880 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,7 +27,7 @@ onlyBuiltDependencies: - '@tailwindcss/oxide' overrides: canvas: 2.11.2 - sharp: ^0.34.2 + sharp: ^0.34.3 packageExtensions: nestjs-kysely: dependencies: diff --git a/readme_i18n/README_ar_JO.md b/readme_i18n/README_ar_JO.md index 4e7ba99dd2..b2c195cd3e 100644 --- a/readme_i18n/README_ar_JO.md +++ b/readme_i18n/README_ar_JO.md @@ -46,13 +46,13 @@ ## محتوى -- [الوثائق الرسمية](https://immich.app/docs) +- [الوثائق الرسمية](https://docs.immich.app) - [خريطة الطريق](https://github.com/orgs/immich-app/projects/1) - [تجريبي](#demo) - [سمات](#features) -- [مقدمة](https://immich.app/docs/overview/introduction) -- [تعليمات التحميل](https://immich.app/docs/install/requirements) -- [قواعد المساهمة](https://immich.app/docs/overview/support-the-project) +- [مقدمة](https://docs.immich.app/overview/introduction) +- [تعليمات التحميل](https://docs.immich.app/install/requirements) +- [قواعد المساهمة](https://docs.immich.app/overview/support-the-project) ## توثيق diff --git a/readme_i18n/README_ca_ES.md b/readme_i18n/README_ca_ES.md index 0b8dd5999b..5efcf51aaf 100644 --- a/readme_i18n/README_ca_ES.md +++ b/readme_i18n/README_ca_ES.md @@ -44,13 +44,13 @@ ## Contingut -- [Documentació oficial](https://immich.app/docs) +- [Documentació oficial](https://docs.immich.app) - [Mapa de ruta](https://github.com/orgs/immich-app/projects/1) - [Demo](#demo) - [Funcionalitats](#funcionalitats) -- [Introducció](https://immich.app/docs/overview/introduction) -- [Instal·lació](https://immich.app/docs/install/requirements) -- [Directrius de contribució](https://immich.app/docs/overview/support-the-project) +- [Introducció](https://docs.immich.app/overview/introduction) +- [Instal·lació](https://docs.immich.app/install/requirements) +- [Directrius de contribució](https://docs.immich.app/overview/support-the-project) ## Documentació diff --git a/readme_i18n/README_de_DE.md b/readme_i18n/README_de_DE.md index d24818b881..e54a1f0f71 100644 --- a/readme_i18n/README_de_DE.md +++ b/readme_i18n/README_de_DE.md @@ -50,14 +50,14 @@ ## Inhalt -- [Offizielle Dokumentation](https://immich.app/docs) -- [Über Immich](https://immich.app/docs/overview/introduction) -- [Installation](https://immich.app/docs/install/requirements) +- [Offizielle Dokumentation](https://docs.immich.app) +- [Über Immich](https://docs.immich.app/overview/introduction) +- [Installation](https://docs.immich.app/install/requirements) - [Roadmap](https://github.com/orgs/immich-app/projects/1) - [Demo](#demo) - [Funktionen](#funktionen) -- [Übersetzungen](https://immich.app/docs/developer/translations) -- [Beitragsrichtlinien](https://immich.app/docs/overview/support-the-project) +- [Übersetzungen](https://docs.immich.app/developer/translations) +- [Beitragsrichtlinien](https://docs.immich.app/overview/support-the-project) ## Demo @@ -107,7 +107,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App ## Übersetzungen -Mehr zum Thema Übersetzungen kannst du [hier](https://immich.app/docs/developer/translations) erfahren. +Mehr zum Thema Übersetzungen kannst du [hier](https://docs.immich.app/developer/translations) erfahren. Translation status diff --git a/readme_i18n/README_es_ES.md b/readme_i18n/README_es_ES.md index 00417c9188..0b4da86f71 100644 --- a/readme_i18n/README_es_ES.md +++ b/readme_i18n/README_es_ES.md @@ -45,13 +45,13 @@ ## Contenido -- [Documentación oficial](https://immich.app/docs) +- [Documentación oficial](https://docs.immich.app) - [Hoja de ruta](https://github.com/orgs/immich-app/projects/1) - [Demo](#demo) - [Funciones](#funciones) -- [Introducción](https://immich.app/docs/overview/introduction) -- [Instalación](https://immich.app/docs/install/requirements) -- [Directrices para contribuir](https://immich.app/docs/overview/support-the-project) +- [Introducción](https://docs.immich.app/overview/introduction) +- [Instalación](https://docs.immich.app/install/requirements) +- [Directrices para contribuir](https://docs.immich.app/overview/support-the-project) ## Documentación @@ -99,7 +99,7 @@ contraseña: demo ## Traducciones -Lea mas acerca de las traducciones [acá](https://immich.app/docs/developer/translations). +Lea mas acerca de las traducciones [acá](https://docs.immich.app/developer/translations). Translation status diff --git a/readme_i18n/README_fr_FR.md b/readme_i18n/README_fr_FR.md index e1d4fb1cbc..ae565571f8 100644 --- a/readme_i18n/README_fr_FR.md +++ b/readme_i18n/README_fr_FR.md @@ -45,13 +45,13 @@ ## Sommaire -- [Documentation officielle](https://immich.app/docs) +- [Documentation officielle](https://docs.immich.app) - [Feuille de route](https://github.com/orgs/immich-app/projects/1) - [Démo](#démo) - [Fonctionnalités](#fonctionnalités) -- [Introduction](https://immich.app/docs/overview/introduction) -- [Installation](https://immich.app/docs/install/requirements) -- [Contribution](https://immich.app/docs/overview/support-the-project) +- [Introduction](https://docs.immich.app/overview/introduction) +- [Installation](https://docs.immich.app/install/requirements) +- [Contribution](https://docs.immich.app/overview/support-the-project) ## Documentation diff --git a/readme_i18n/README_it_IT.md b/readme_i18n/README_it_IT.md index 5a368fe1f9..656b392616 100644 --- a/readme_i18n/README_it_IT.md +++ b/readme_i18n/README_it_IT.md @@ -49,14 +49,14 @@ ## Link utili -- [Documentazione](https://immich.app/docs) -- [Informazioni](https://immich.app/docs/overview/introduction) -- [Installazione](https://immich.app/docs/install/requirements) +- [Documentazione](https://docs.immich.app) +- [Informazioni](https://docs.immich.app/overview/introduction) +- [Installazione](https://docs.immich.app/install/requirements) - [Roadmap](https://immich.app/roadmap) - [Demo](#demo) - [Funzionalità](#funzionalità) -- [Traduzioni](https://immich.app/docs/developer/translations) -- [Contribuire](https://immich.app/docs/overview/support-the-project) +- [Traduzioni](https://docs.immich.app/developer/translations) +- [Contribuire](https://docs.immich.app/overview/support-the-project) ## Demo @@ -106,7 +106,7 @@ Per l’app mobile puoi usare `https://demo.immich.app` come `Server Endpoint UR ## Traduzioni -Scopri di più sulle traduzioni [qui](https://immich.app/docs/developer/translations). +Scopri di più sulle traduzioni [qui](https://docs.immich.app/developer/translations). Stato traduzioni diff --git a/readme_i18n/README_ja_JP.md b/readme_i18n/README_ja_JP.md index 60dd0f3ed7..a6fa4953af 100644 --- a/readme_i18n/README_ja_JP.md +++ b/readme_i18n/README_ja_JP.md @@ -44,13 +44,13 @@ ## コンテンツ -- [公式ドキュメント](https://immich.app/docs) +- [公式ドキュメント](https://docs.immich.app) - [ロードマップ](https://github.com/orgs/immich-app/projects/1) - [デモ](#デモ) - [機能](#機能) -- [紹介](https://immich.app/docs/overview/introduction) -- [インストール](https://immich.app/docs/install/requirements) -- [コントリビューションガイド](https://immich.app/docs/overview/support-the-project) +- [紹介](https://docs.immich.app/overview/introduction) +- [インストール](https://docs.immich.app/install/requirements) +- [コントリビューションガイド](https://docs.immich.app/overview/support-the-project) ## ドキュメント diff --git a/readme_i18n/README_ko_KR.md b/readme_i18n/README_ko_KR.md index 031e2fd9ca..f4feb29fcc 100644 --- a/readme_i18n/README_ko_KR.md +++ b/readme_i18n/README_ko_KR.md @@ -50,14 +50,14 @@ ## 링크 -- [문서](https://immich.app/docs) -- [소개](https://immich.app/docs/overview/introduction) -- [설치](https://immich.app/docs/install/requirements) +- [문서](https://docs.immich.app) +- [소개](https://docs.immich.app/overview/introduction) +- [설치](https://docs.immich.app/install/requirements) - [로드맵](https://immich.app/roadmap) - [데모](#데모) - [기능](#기능) -- [번역](https://immich.app/docs/developer/tranlations) -- [기여](https://immich.app/docs/overview/support-the-project) +- [번역](https://docs.immich.app/developer/tranlations) +- [기여](https://docs.immich.app/overview/support-the-project) ## 데모 @@ -104,7 +104,7 @@ ## 번역 -번역에 대한 자세한 정보는 [이곳](https://immich.app/docs/developer/translations)에서 확인하세요. +번역에 대한 자세한 정보는 [이곳](https://docs.immich.app/developer/translations)에서 확인하세요. 번역 현황 diff --git a/readme_i18n/README_nl_NL.md b/readme_i18n/README_nl_NL.md index 46692bc612..0400442bb4 100644 --- a/readme_i18n/README_nl_NL.md +++ b/readme_i18n/README_nl_NL.md @@ -45,13 +45,13 @@ ## Inhoud -- [Officiële documentatie](https://immich.app/docs) +- [Officiële documentatie](https://docs.immich.app) - [Toekomstplannen](https://github.com/orgs/immich-app/projects/1) - [Demo](#demo) - [Functies](#functies) -- [Introductie](https://immich.app/docs/overview/introduction) -- [Installatie](https://immich.app/docs/install/requirements) -- [Richtlijnen voor bijdragen](https://immich.app/docs/overview/support-the-project) +- [Introductie](https://docs.immich.app/overview/introduction) +- [Installatie](https://docs.immich.app/install/requirements) +- [Richtlijnen voor bijdragen](https://docs.immich.app/overview/support-the-project) ## Documentatie @@ -102,7 +102,7 @@ Je kunt de demo [hier](https://demo.immich.app/) bekijken. Voor de mobiele app k ## Vertalingen -Je kunt [hier](https://immich.app/docs/developer/translations) meer over vertalingen lezen. +Je kunt [hier](https://docs.immich.app/developer/translations) meer over vertalingen lezen. ## Repository activiteit diff --git a/readme_i18n/README_pt_BR.md b/readme_i18n/README_pt_BR.md index 2320e8fd6f..240fe4d2e2 100644 --- a/readme_i18n/README_pt_BR.md +++ b/readme_i18n/README_pt_BR.md @@ -55,14 +55,14 @@ ## Links -- [Documentação](https://immich.app/docs) -- [Sobre](https://immich.app/docs/overview/introduction) -- [Instalação](https://immich.app/docs/install/requirements) +- [Documentação](https://docs.immich.app) +- [Sobre](https://docs.immich.app/overview/introduction) +- [Instalação](https://docs.immich.app/install/requirements) - [Roadmap](https://github.com/orgs/immich-app/projects/1) - [Demonstração](#demonstração) - [Funcionalidades](#funcionalidades) -- [Traduções](https://immich.app/docs/developer/translations) -- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project) +- [Traduções](https://docs.immich.app/developer/translations) +- [Diretrizes de Contribuição](https://docs.immich.app/overview/support-the-project) ## Demonstração @@ -115,7 +115,7 @@ Acesse a demonstração [aqui](https://demo.immich.app). No aplicativo para disp ## Traduções Leia mais sobre as traduções -[aqui](https://immich.app/docs/developer/translations). +[aqui](https://docs.immich.app/developer/translations). Status da tradução diff --git a/readme_i18n/README_ru_RU.md b/readme_i18n/README_ru_RU.md index be97259acc..d774fae84c 100644 --- a/readme_i18n/README_ru_RU.md +++ b/readme_i18n/README_ru_RU.md @@ -51,14 +51,14 @@ ## Содержание -- [Официальная документация](https://immich.app/docs) -- [Введение](https://immich.app/docs/overview/introduction) -- [Установка](https://immich.app/docs/install/requirements) +- [Официальная документация](https://docs.immich.app) +- [Введение](https://docs.immich.app/overview/introduction) +- [Установка](https://docs.immich.app/install/requirements) - [План разработки](https://github.com/orgs/immich-app/projects/1) - [Демо](#demo) - [Возможности](#features) -- [Перевод](https://immich.app/docs/developer/translations) -- [Гид по участию и поддержке проекта](https://immich.app/docs/overview/support-the-project) +- [Перевод](https://docs.immich.app/developer/translations) +- [Гид по участию и поддержке проекта](https://docs.immich.app/overview/support-the-project) ## Демо @@ -107,7 +107,7 @@ ## Перевод -Всё про перевод проекта [Здесь](https://immich.app/docs/developer/translations). +Всё про перевод проекта [Здесь](https://docs.immich.app/developer/translations). Translation status diff --git a/readme_i18n/README_sv_SE.md b/readme_i18n/README_sv_SE.md index daa68b9874..24c08fa8df 100644 --- a/readme_i18n/README_sv_SE.md +++ b/readme_i18n/README_sv_SE.md @@ -46,13 +46,13 @@ ## Innehåll -- [Officiell Dokumentation](https://immich.app/docs) +- [Officiell Dokumentation](https://docs.immich.app) - [Roadmap](https://github.com/orgs/immich-app/projects/1) - [Demo](#demo) - [Funktioner](#features) -- [Introduktion](https://immich.app/docs/overview/introduction) -- [Installation](https://immich.app/docs/install/requirements) -- [Riktlinjer för Bidrag](https://immich.app/docs/overview/support-the-project) +- [Introduktion](https://docs.immich.app/overview/introduction) +- [Installation](https://docs.immich.app/install/requirements) +- [Riktlinjer för Bidrag](https://docs.immich.app/overview/support-the-project) ## Dokumentation diff --git a/readme_i18n/README_th_TH.md b/readme_i18n/README_th_TH.md index bdb6db868d..8d34261281 100644 --- a/readme_i18n/README_th_TH.md +++ b/readme_i18n/README_th_TH.md @@ -52,14 +52,14 @@ ## ลิงก์ -- [คู่มือ](https://immich.app/docs) -- [เกี่ยวกับ](https://immich.app/docs/overview/introduction) -- [การติดตั้ง](https://immich.app/docs/install/requirements) +- [คู่มือ](https://docs.immich.app) +- [เกี่ยวกับ](https://docs.immich.app/overview/introduction) +- [การติดตั้ง](https://docs.immich.app/install/requirements) - [โรดแมป](https://immich.app/roadmap) - [สาธิต](#สาธิต) - [คุณสมบัติ](#คุณสมบัติ) -- [การแปลภาษา](https://immich.app/docs/developer/translations) -- [สนับสนุนโพรเจกต์](https://immich.app/docs/overview/support-the-project) +- [การแปลภาษา](https://docs.immich.app/developer/translations) +- [สนับสนุนโพรเจกต์](https://docs.immich.app/overview/support-the-project) ## สาธิต @@ -106,7 +106,7 @@ ## การแปลภาษา -อ่านเพิ่มเติมเกี่ยวกับการแปล [ที่นี่](https://immich.app/docs/developer/translations) +อ่านเพิ่มเติมเกี่ยวกับการแปล [ที่นี่](https://docs.immich.app/developer/translations) สถานะการแปล diff --git a/readme_i18n/README_tr_TR.md b/readme_i18n/README_tr_TR.md index 6285ab55a2..930d750b88 100644 --- a/readme_i18n/README_tr_TR.md +++ b/readme_i18n/README_tr_TR.md @@ -44,13 +44,13 @@ ## Content -- [Resmi Belgeler](https://immich.app/docs) +- [Resmi Belgeler](https://docs.immich.app) - [Yol Haritası](https://github.com/orgs/immich-app/projects/1) - [Demo](#demo) - [Özellikler](#özellikler) -- [Giriş](https://immich.app/docs/overview/introduction) -- [Kurulum](https://immich.app/docs/install/requirements) -- [Katkı Sağlama Rehberi](https://immich.app/docs/overview/support-the-project) +- [Giriş](https://docs.immich.app/overview/introduction) +- [Kurulum](https://docs.immich.app/install/requirements) +- [Katkı Sağlama Rehberi](https://docs.immich.app/overview/support-the-project) ## Belgeler diff --git a/readme_i18n/README_uk_UA.md b/readme_i18n/README_uk_UA.md index 5a33fa210d..f236d49091 100644 --- a/readme_i18n/README_uk_UA.md +++ b/readme_i18n/README_uk_UA.md @@ -42,26 +42,26 @@ - ⚠️ Цей проєкт перебуває **в дуже активній** розробці. - ⚠️ Очікуйте безліч помилок і глобальних змін. -- ⚠️ **Не використовуйте цей додаток як єдине сховище своїх фото та відео.** +- ⚠️ **Не використовуйте цей застосунок як єдине сховище своїх фото та відео.** - ⚠️ Завжди дотримуйтесь [плану резервного копіювання 3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) для ваших дорогоцінних фотографій та відео! > [!NOTE] -> Основну документацію, зокрема посібники з встановлення, можна знайти за адресою https://immich.app/. +> Основну документацію, зокрема посібники зі встановлення, можна знайти за адресою https://immich.app/. ## Посилання -- [Документація](https://immich.app/docs) -- [Про проєкт](https://immich.app/docs/overview/introduction) -- [Встановлення](https://immich.app/docs/install/requirements) +- [Документація](https://docs.immich.app) +- [Про проєкт](https://docs.immich.app/overview/introduction) +- [Встановлення](https://docs.immich.app/install/requirements) - [Дорожня карта](https://immich.app/roadmap) - [Демо](#демо) - [Функції](#функції) -- [Переклади](https://immich.app/docs/developer/translations) -- [Гід для розробки проєкту](https://immich.app/docs/overview/support-the-project) +- [Переклади](https://docs.immich.app/developer/translations) +- [Гід для розробки проєкту](https://docs.immich.app/overview/support-the-project) ## Демо -Доступ до демо-версії [тут](https://demo.immich.app). Для мобільного додатку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`. +Доступ до демо-версії [тут](https://demo.immich.app). Для мобільного застосунку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`. ### Облікові дані для входу @@ -74,7 +74,7 @@ | Функції | Додаток | Веб | | :------------------------------------------------------- | ------- | --- | | Завантаження та перегляд відео й фото | Так | Так | -| Автоматичне резервне копіювання при відкритті додатка | Так | Н/Д | +| Автоматичне резервне копіювання при відкритті застосунку | Так | Н/Д | | Запобігання дублюванню файлів | Так | Так | | Вибір альбомів для резервного копіювання | Так | Н/Д | | Завантаження фото та відео на локальний пристрій | Так | Так | @@ -106,13 +106,13 @@ ## Переклади -Більше про переклади [тут](https://immich.app/docs/developer/translations). +Більше про переклади [тут](https://docs.immich.app/developer/translations). Статус перекладів -## Активність репозитарію +## Активність репозиторію ![Діяльність](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Зображення аналітики Repobeats") diff --git a/readme_i18n/README_vi_VN.md b/readme_i18n/README_vi_VN.md index fd04bd9fa1..f74e7c3771 100644 --- a/readme_i18n/README_vi_VN.md +++ b/readme_i18n/README_vi_VN.md @@ -52,14 +52,14 @@ ## Liên kết -- [Tài liệu](https://immich.app/docs) -- [Giới thiệu](https://immich.app/docs/overview/introduction) -- [Cài đặt](https://immich.app/docs/install/requirements) +- [Tài liệu](https://docs.immich.app) +- [Giới thiệu](https://docs.immich.app/overview/introduction) +- [Cài đặt](https://docs.immich.app/install/requirements) - [Lộ trình](https://immich.app/roadmap) - [Demo](#demo) - [Tính năng](#Tính-năng) -- [Dịch thuật](https://immich.app/docs/developer/translations) -- [Đóng góp](https://immich.app/docs/overview/support-the-project) +- [Dịch thuật](https://docs.immich.app/developer/translations) +- [Đóng góp](https://docs.immich.app/overview/support-the-project) ## Demo @@ -106,7 +106,7 @@ Truy cập bản demo [tại đây](https://demo.immich.app). Đối với ứng ## Dịch thuật -Đọc thêm về dịch thuật [tại đây](https://immich.app/docs/developer/translations). +Đọc thêm về dịch thuật [tại đây](https://docs.immich.app/developer/translations). Tình trạng dịch thuật diff --git a/readme_i18n/README_zh_CN.md b/readme_i18n/README_zh_CN.md index 5151e35379..c95631db89 100644 --- a/readme_i18n/README_zh_CN.md +++ b/readme_i18n/README_zh_CN.md @@ -54,14 +54,14 @@ ## 目录 -- [官方文档](https://immich.app/docs) -- [项目总览](https://immich.app/docs/overview/introduction) -- [安装教程](https://immich.app/docs/install/requirements) +- [官方文档](https://docs.immich.app) +- [项目总览](https://docs.immich.app/overview/introduction) +- [安装教程](https://docs.immich.app/install/requirements) - [路线图](https://immich.app/roadmap) - [在线演示](#示例) - [功能特性](#功能特性) -- [多语言](https://immich.app/docs/developer/translations) -- [贡献者](https://immich.app/docs/overview/support-the-project) +- [多语言](https://docs.immich.app/developer/translations) +- [贡献者](https://docs.immich.app/overview/support-the-project) ## 示例 @@ -110,7 +110,7 @@ ## 多语言 -关于翻译的更多信息请参见[此处](https://immich.app/docs/developer/translations)。 +关于翻译的更多信息请参见[此处](https://docs.immich.app/developer/translations)。 翻译进度 diff --git a/server/.nvmrc b/server/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/server/Dockerfile b/server/Dockerfile index bd5c55402e..6702b338c5 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,83 +1,4 @@ -# dev build -FROM ghcr.io/immich-app/base-server-dev:202508191104@sha256:0608857ef682099c458f0fb319afdcaf09462bbb5670b6dcd3642029f12eee1c AS dev - -ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ - CI=1 \ - COREPACK_HOME=/tmp - -RUN npm install --global corepack@latest && \ - corepack enable pnpm && \ - echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ - echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc - -COPY ./package* ./pnpm* .pnpmfile.cjs /tmp/create-dep-cache/ -COPY ./web/package* ./web/pnpm* /tmp/create-dep-cache/web/ -COPY ./server/package* ./server/pnpm* /tmp/create-dep-cache/server/ -COPY ./open-api/typescript-sdk/package* ./open-api/typescript-sdk/pnpm* /tmp/create-dep-cache/open-api/typescript-sdk/ -WORKDIR /tmp/create-dep-cache -RUN pnpm fetch && rm -rf /tmp/create-dep-cache && chmod -R o+rw /buildcache -WORKDIR /usr/src/app - -ENV PATH="${PATH}:/usr/src/app/server/bin:/usr/src/app/web/bin" \ - IMMICH_ENV=development \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all -ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] - -FROM dev AS dev-container-server - -RUN apt-get update --allow-releaseinfo-change && \ - apt-get install sudo inetutils-ping openjdk-11-jre-headless \ - vim nano \ - -y --no-install-recommends --fix-missing - -RUN usermod -aG sudo node && \ - echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ - mkdir -p /workspaces/immich - -RUN chown node:node -R /workspaces -COPY --chown=node:node --chmod=755 ../.devcontainer/server/*.sh /immich-devcontainer/ - -WORKDIR /workspaces/immich - -FROM dev-container-server AS dev-container-mobile -USER root -# Enable multiarch for arm64 if necessary -RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \ - dpkg --add-architecture amd64 && \ - apt-get update && \ - apt-get install -y --no-install-recommends \ - qemu-user-static \ - libc6:amd64 \ - libstdc++6:amd64 \ - libgcc1:amd64; \ - fi - -# Flutter SDK -# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux -ENV FLUTTER_CHANNEL="stable" -ENV FLUTTER_VERSION="3.32.8" -ENV FLUTTER_HOME=/flutter -ENV PATH=${PATH}:${FLUTTER_HOME}/bin - -# Flutter SDK -RUN mkdir -p ${FLUTTER_HOME} \ - && curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \ - && tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \ - && rm flutter.tar.xz \ - && chown -R node ${FLUTTER_HOME} - - -RUN apt-get update \ - && wget -qO- https://dcm.dev/pgp-key.public | gpg --dearmor -o /usr/share/keyrings/dcm.gpg \ - && echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | tee /etc/apt/sources.list.d/dart_stable.list \ - && apt-get update \ - && apt-get install dcm -y - -RUN dart --disable-analytics - -# production-builder-base image -FROM ghcr.io/immich-app/base-server-dev:202508191104@sha256:0608857ef682099c458f0fb319afdcaf09462bbb5670b6dcd3642029f12eee1c AS prod-builder-base +FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS builder ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ CI=1 \ COREPACK_HOME=/tmp @@ -85,8 +6,7 @@ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ RUN npm install --global corepack@latest && \ corepack enable pnpm -# server production build -FROM prod-builder-base AS server-prod +FROM builder AS server WORKDIR /usr/src/app COPY ./package* ./pnpm* .pnpmfile.cjs ./ @@ -94,8 +14,7 @@ COPY ./server ./server/ RUN SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \ SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned -# web production build -FROM prod-builder-base AS web-prod +FROM builder AS web WORKDIR /usr/src/app COPY ./package* ./pnpm* .pnpmfile.cjs ./ @@ -105,7 +24,7 @@ COPY ./open-api ./open-api/ RUN SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter immich-web --frozen-lockfile --force install && \ pnpm --filter @immich/sdk --filter immich-web build -FROM prod-builder-base AS cli-prod +FROM builder AS cli COPY ./package* ./pnpm* .pnpmfile.cjs ./ COPY ./cli ./cli/ @@ -114,18 +33,17 @@ RUN pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install && pnpm --filter @immich/sdk --filter @immich/cli build && \ pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned -# prod base image -FROM ghcr.io/immich-app/base-server-prod:202508191104@sha256:4cce4119f5555fce5e383b681e4feea31956ceadb94cafcbcbbae2c7b94a1b62 +FROM ghcr.io/immich-app/base-server-prod:202509210934@sha256:0c7eacf0ba88ca52e1a267cfc62d20d07792ea2c604818c2cbd37dc7dcefdac9 WORKDIR /usr/src/app ENV NODE_ENV=production \ NVIDIA_DRIVER_CAPABILITIES=all \ NVIDIA_VISIBLE_DEVICES=all -COPY --from=server-prod /output/server-pruned ./server -COPY --from=web-prod /usr/src/app/web/build /build/www -COPY --from=cli-prod /output/cli-pruned ./cli -RUN ln -s ./cli/bin/immich server/bin/immich +COPY --from=server /output/server-pruned ./server +COPY --from=web /usr/src/app/web/build /build/www +COPY --from=cli /output/cli-pruned ./cli +RUN ln -s ../../cli/bin/immich server/bin/immich COPY LICENSE /licenses/LICENSE.txt COPY LICENSE /LICENSE diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev new file mode 100644 index 0000000000..dd2e931745 --- /dev/null +++ b/server/Dockerfile.dev @@ -0,0 +1,77 @@ +# dev build +FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS dev + +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + CI=1 \ + COREPACK_HOME=/tmp + +RUN npm install --global corepack@latest && \ + corepack enable pnpm && \ + echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ + echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc + +COPY ./package* ./pnpm* .pnpmfile.cjs /tmp/create-dep-cache/ +COPY ./web/package* ./web/pnpm* /tmp/create-dep-cache/web/ +COPY ./server/package* ./server/pnpm* /tmp/create-dep-cache/server/ +COPY ./open-api/typescript-sdk/package* ./open-api/typescript-sdk/pnpm* /tmp/create-dep-cache/open-api/typescript-sdk/ +WORKDIR /tmp/create-dep-cache +RUN pnpm fetch && rm -rf /tmp/create-dep-cache && chmod -R o+rw /buildcache +WORKDIR /usr/src/app + +ENV PATH="${PATH}:/usr/src/app/server/bin:/usr/src/app/web/bin" \ + IMMICH_ENV=development \ + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all +ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] + +FROM dev AS dev-container-server + +RUN apt-get update --allow-releaseinfo-change && \ + apt-get install sudo inetutils-ping openjdk-11-jre-headless \ + vim nano \ + -y --no-install-recommends --fix-missing + +RUN usermod -aG sudo node && \ + echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ + mkdir -p /workspaces/immich + +RUN chown node:node -R /workspaces +COPY --chown=node:node --chmod=755 ../.devcontainer/server/*.sh /immich-devcontainer/ + +WORKDIR /workspaces/immich + +FROM dev-container-server AS dev-container-mobile +USER root +# Enable multiarch for arm64 if necessary +RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \ + dpkg --add-architecture amd64 && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + qemu-user-static \ + libc6:amd64 \ + libstdc++6:amd64 \ + libgcc1:amd64; \ + fi + +# Flutter SDK +# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux +ENV FLUTTER_CHANNEL="stable" +ENV FLUTTER_VERSION="3.35.4" +ENV FLUTTER_HOME=/flutter +ENV PATH=${PATH}:${FLUTTER_HOME}/bin + +# Flutter SDK +RUN mkdir -p ${FLUTTER_HOME} \ + && curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \ + && tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \ + && rm flutter.tar.xz \ + && chown -R node ${FLUTTER_HOME} + + +RUN apt-get update \ + && wget -qO- https://dcm.dev/pgp-key.public | gpg --dearmor -o /usr/share/keyrings/dcm.gpg \ + && echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | tee /etc/apt/sources.list.d/dart_stable.list \ + && apt-get update \ + && apt-get install dcm -y + +RUN dart --disable-analytics diff --git a/server/bin/start.sh b/server/bin/start.sh index 10f897dd8e..0afff4c3a8 100755 --- a/server/bin/start.sh +++ b/server/bin/start.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash echo "Initializing Immich $IMMICH_SOURCE_REF" -lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2" -if [ -f "$lib_path" ]; then - export LD_PRELOAD="$lib_path" -else - echo "skipping libmimalloc - path not found $lib_path" -fi +# TODO: Update to mimalloc v3 when verified memory isn't released issue is fixed +# lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.3" +# if [ -f "$lib_path" ]; then +# export LD_PRELOAD="$lib_path" +# else +# echo "skipping libmimalloc - path not found $lib_path" +# fi export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib" -SERVER_HOME=/usr/src/app/server +SERVER_HOME="$(readlink -f "$(dirname "$0")/..")" read_file_and_export() { fname="${!1}" diff --git a/server/package.json b/server/package.json index 5ac0a8f043..e2845b82ef 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.139.4", + "version": "2.0.0", "description": "", "author": "", "private": true, @@ -37,23 +37,21 @@ "@nestjs/bullmq": "^11.0.1", "@nestjs/common": "^11.0.4", "@nestjs/core": "^11.0.4", - "@nestjs/event-emitter": "^3.0.0", "@nestjs/platform-express": "^11.0.4", "@nestjs/platform-socket.io": "^11.0.4", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/auto-instrumentations-node": "^0.62.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.203.0", - "@opentelemetry/instrumentation-http": "^0.203.0", - "@opentelemetry/instrumentation-ioredis": "^0.51.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.49.0", - "@opentelemetry/instrumentation-pg": "^0.56.0", + "@opentelemetry/exporter-prometheus": "^0.205.0", + "@opentelemetry/instrumentation-http": "^0.205.0", + "@opentelemetry/instrumentation-ioredis": "^0.53.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.51.0", + "@opentelemetry/instrumentation-pg": "^0.58.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-node": "^0.205.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", @@ -103,25 +101,21 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.34.2", + "sharp": "^0.34.3", "sirv": "^3.0.0", "socket.io": "^4.8.1", "tailwindcss-preset-email": "^1.4.0", "thumbhash": "^0.1.1", - "typeorm": "^0.3.17", "ua-parser-js": "^2.0.0", "uuid": "^11.1.0", "validator": "^13.12.0" }, "devDependencies": { - "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.8.0", "@nestjs/cli": "^11.0.2", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.4", "@swc/core": "^1.4.14", - "@testcontainers/postgresql": "^11.0.0", - "@testcontainers/redis": "^11.0.0", "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^6.0.0", @@ -135,8 +129,8 @@ "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", "@types/multer": "^2.0.0", - "@types/node": "^22.13.14", - "@types/nodemailer": "^6.4.14", + "@types/node": "^22.18.1", + "@types/nodemailer": "^7.0.0", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", "@types/react": "^19.0.0", @@ -146,36 +140,30 @@ "@types/ua-parser-js": "^0.7.36", "@types/validator": "^13.15.2", "@vitest/coverage-v8": "^3.0.0", - "canvas": "^3.1.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^60.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", - "node-addon-api": "^8.3.1", "node-gyp": "^11.2.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", - "rimraf": "^6.0.0", - "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", "supertest": "^7.1.0", "tailwindcss": "^3.4.0", "testcontainers": "^11.0.0", - "tsconfig-paths": "^4.2.0", "typescript": "^5.9.2", "typescript-eslint": "^8.28.0", "unplugin-swc": "^1.4.5", - "utimes": "^5.2.1", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0" }, "volta": { - "node": "22.18.0" + "node": "22.20.0" }, "overrides": { - "sharp": "^0.34.2" + "sharp": "^0.34.3" } } diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 6d3cb42fae..b632332069 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -15,6 +15,7 @@ import { repositories } from 'src/repositories'; import { AccessRepository } from 'src/repositories/access.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; import { SyncRepository } from 'src/repositories/sync.repository'; import { AuthService } from 'src/services/auth.service'; import { getKyselyConfig } from 'src/utils/database'; @@ -57,7 +58,7 @@ class SqlGenerator { try { await this.setup(); for (const Repository of repositories) { - if (Repository === LoggingRepository) { + if (Repository === LoggingRepository || Repository === MachineLearningRepository) { continue; } await this.process(Repository); diff --git a/server/src/config.ts b/server/src/config.ts index 33a6f19ba1..66c03450fa 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -54,6 +54,11 @@ export interface SystemConfig { machineLearning: { enabled: boolean; urls: string[]; + availabilityChecks: { + enabled: boolean; + timeout: number; + interval: number; + }; clip: { enabled: boolean; modelName: string; @@ -176,6 +181,8 @@ export interface SystemConfig { }; } +export type MachineLearningConfig = SystemConfig['machineLearning']; + export const defaults = Object.freeze({ backup: { database: { @@ -191,7 +198,7 @@ export const defaults = Object.freeze({ targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], targetAudioCodec: AudioCodec.Aac, - acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le], + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus], acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], targetResolution: '720', maxBitrate: '0', @@ -227,6 +234,11 @@ export const defaults = Object.freeze({ machineLearning: { enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], + availabilityChecks: { + enabled: true, + timeout: Number(process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT) || 2000, + interval: 30_000, + }, clip: { enabled: true, modelName: 'ViT-B-32__openai', diff --git a/server/src/constants.ts b/server/src/constants.ts index b47640c4ae..1bae521a9f 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -51,11 +51,6 @@ export const serverVersion = new SemVer(version); export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); export const ONE_HOUR = Duration.fromObject({ hours: 1 }); -export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000); -export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number( - process.env.MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME || 30_000, -); - export const citiesFile = 'cities500.txt'; export const reverseGeocodeMaxDistance = 25_000; diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index 67bdeff222..eb594fbe47 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -1,4 +1,6 @@ import { AssetMediaController } from 'src/controllers/asset-media.controller'; +import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto'; +import { AssetMetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { AssetMediaService } from 'src/services/asset-media.service'; import request from 'supertest'; @@ -11,7 +13,7 @@ const makeUploadDto = (options?: { omit: string }): Record => { deviceId: 'TEST', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), - isFavorite: 'testing', + isFavorite: 'false', duration: '0:00:00.000000', }; @@ -27,16 +29,20 @@ describe(AssetMediaController.name, () => { let ctx: ControllerContext; const assetData = Buffer.from('123'); const filename = 'example.png'; + const service = mockBaseService(AssetMediaService); beforeAll(async () => { ctx = await controllerSetup(AssetMediaController, [ { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, - { provide: AssetMediaService, useValue: mockBaseService(AssetMediaService) }, + { provide: AssetMediaService, useValue: service }, ]); return () => ctx.close(); }); beforeEach(() => { + service.resetAllMocks(); + service.uploadAsset.mockResolvedValue({ status: AssetMediaStatus.DUPLICATE, id: factory.uuid() }); + ctx.reset(); }); @@ -46,13 +52,61 @@ describe(AssetMediaController.name, () => { expect(ctx.authenticate).toHaveBeenCalled(); }); + it('should accept metadata', async () => { + const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } }; + const { status } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ + ...makeUploadDto(), + metadata: JSON.stringify([mobileMetadata]), + }); + + expect(service.uploadAsset).toHaveBeenCalledWith( + undefined, + expect.objectContaining({ metadata: [mobileMetadata] }), + expect.objectContaining({ originalName: 'example.png' }), + undefined, + ); + + expect(status).toBe(200); + }); + + it('should handle invalid metadata json', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ + ...makeUploadDto(), + metadata: 'not-a-string-string', + }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['metadata must be valid JSON'])); + }); + + it('should validate iCloudId is a string', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ + ...makeUploadDto(), + metadata: JSON.stringify([{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 123 } }]), + }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['metadata.0.value.iCloudId must be a string'])); + }); + it('should require `deviceAssetId`', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') .attach('assetData', assetData, filename) .field({ ...makeUploadDto({ omit: 'deviceAssetId' }) }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); + expect(body).toEqual( + factory.responses.badRequest(['deviceAssetId must be a string', 'deviceAssetId should not be empty']), + ); }); it('should require `deviceId`', async () => { @@ -61,7 +115,7 @@ describe(AssetMediaController.name, () => { .attach('assetData', assetData, filename) .field({ ...makeUploadDto({ omit: 'deviceId' }) }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); + expect(body).toEqual(factory.responses.badRequest(['deviceId must be a string', 'deviceId should not be empty'])); }); it('should require `fileCreatedAt`', async () => { @@ -70,25 +124,20 @@ describe(AssetMediaController.name, () => { .attach('assetData', assetData, filename) .field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); + expect(body).toEqual( + factory.responses.badRequest(['fileCreatedAt must be a Date instance', 'fileCreatedAt should not be empty']), + ); }); it('should require `fileModifiedAt`', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') .attach('assetData', assetData, filename) - .field({ ...makeUploadDto({ omit: 'fileModifiedAt' }) }); + .field(makeUploadDto({ omit: 'fileModifiedAt' })); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); - }); - - it('should require `duration`', async () => { - const { status, body } = await request(ctx.getHttpServer()) - .post('/assets') - .attach('assetData', assetData, filename) - .field({ ...makeUploadDto({ omit: 'duration' }) }); - expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); + expect(body).toEqual( + factory.responses.badRequest(['fileModifiedAt must be a Date instance', 'fileModifiedAt should not be empty']), + ); }); it('should throw if `isFavorite` is not a boolean', async () => { @@ -97,16 +146,18 @@ describe(AssetMediaController.name, () => { .attach('assetData', assetData, filename) .field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); + expect(body).toEqual(factory.responses.badRequest(['isFavorite must be a boolean value'])); }); it('should throw if `visibility` is not an enum', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') .attach('assetData', assetData, filename) - .field({ ...makeUploadDto(), visibility: 'not-a-boolean' }); + .field({ ...makeUploadDto(), visibility: 'not-an-option' }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest()); + expect(body).toEqual( + factory.responses.badRequest([expect.stringContaining('visibility must be one of the following values:')]), + ); }); // TODO figure out how to deal with `sendFile` diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index 3b216aca0c..688e513b64 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -96,8 +96,9 @@ export class AssetMediaController { @Put(':id/original') @UseInterceptors(FileUploadInterceptor) @ApiConsumes('multipart/form-data') - @EndpointLifecycle({ addedAt: 'v1.106.0' }) - @ApiOperation({ + @EndpointLifecycle({ + addedAt: 'v1.106.0', + deprecatedAt: 'v1.142.0', summary: 'replaceAsset', description: 'Replace the asset with new file, without changing its id', }) @@ -188,7 +189,7 @@ export class AssetMediaController { * Checks if assets exist by checksums */ @Post('bulk-upload-check') - @Authenticated() + @Authenticated({ permission: Permission.AssetUpload }) @ApiOperation({ summary: 'checkBulkUpload', description: 'Checks if assets exist by checksums', diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 66d2d7c206..7a7a37fe2e 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -1,4 +1,5 @@ import { AssetController } from 'src/controllers/asset.controller'; +import { AssetMetadataKey } from 'src/enum'; import { AssetService } from 'src/services/asset.service'; import request from 'supertest'; import { factory } from 'test/small.factory'; @@ -6,14 +7,16 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils' describe(AssetController.name, () => { let ctx: ControllerContext; + const service = mockBaseService(AssetService); beforeAll(async () => { - ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]); + ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: service }]); return () => ctx.close(); }); beforeEach(() => { ctx.reset(); + service.resetAllMocks(); }); describe('PUT /assets', () => { @@ -115,4 +118,120 @@ describe(AssetController.name, () => { ); }); }); + + describe('GET /assets/:id/metadata', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /assets/:id/metadata', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({ items: [] }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + }); + + it('should require items to be an array', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({}); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['items must be an array'])); + }); + + it('should require each item to have a valid key', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/${factory.uuid()}/metadata`) + .send({ items: [{ key: 'someKey' }] }); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining([expect.stringContaining('items.0.key must be one of the following values')]), + ), + ); + }); + + it('should require each item to have a value', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/${factory.uuid()}/metadata`) + .send({ items: [{ key: 'mobile-app', value: null }] }); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest(expect.arrayContaining([expect.stringContaining('value must be an object')])), + ); + }); + + describe(AssetMetadataKey.MobileApp, () => { + it('should accept valid data and pass to service correctly', async () => { + const assetId = factory.uuid(); + const { status } = await request(ctx.getHttpServer()) + .put(`/assets/${assetId}/metadata`) + .send({ items: [{ key: 'mobile-app', value: { iCloudId: '123' } }] }); + expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, { + items: [{ key: 'mobile-app', value: { iCloudId: '123' } }], + }); + expect(status).toBe(200); + }); + + it('should work without iCloudId', async () => { + const assetId = factory.uuid(); + const { status } = await request(ctx.getHttpServer()) + .put(`/assets/${assetId}/metadata`) + .send({ items: [{ key: 'mobile-app', value: {} }] }); + expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, { + items: [{ key: 'mobile-app', value: {} }], + }); + expect(status).toBe(200); + }); + }); + }); + + describe('GET /assets/:id/metadata/:key', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/mobile-app`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + }); + + it('should require a valid key', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/invalid`); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining([expect.stringContaining('key must be one of the following value')]), + ), + ); + }); + }); + + describe('DELETE /assets/:id/metadata/:key', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/mobile-app`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + + it('should require a valid key', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/invalid`); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest([expect.stringContaining('key must be one of the following values')]), + ); + }); + }); }); diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index edb5aab602..1f320f6595 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -6,6 +6,9 @@ import { AssetBulkDeleteDto, AssetBulkUpdateDto, AssetJobsDto, + AssetMetadataResponseDto, + AssetMetadataRouteParams, + AssetMetadataUpsertDto, AssetStatsDto, AssetStatsResponseDto, DeviceIdDto, @@ -85,4 +88,36 @@ export class AssetController { ): Promise { return this.service.update(auth, id, dto); } + + @Get(':id/metadata') + @Authenticated({ permission: Permission.AssetRead }) + getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getMetadata(auth, id); + } + + @Put(':id/metadata') + @Authenticated({ permission: Permission.AssetUpdate }) + updateAssetMetadata( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetMetadataUpsertDto, + ): Promise { + return this.service.upsertMetadata(auth, id, dto); + } + + @Get(':id/metadata/:key') + @Authenticated({ permission: Permission.AssetRead }) + getAssetMetadataByKey( + @Auth() auth: AuthDto, + @Param() { id, key }: AssetMetadataRouteParams, + ): Promise { + return this.service.getMetadataByKey(auth, id, key); + } + + @Delete(':id/metadata/:key') + @Authenticated({ permission: Permission.AssetUpdate }) + @HttpCode(HttpStatus.NO_CONTENT) + deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise { + return this.service.deleteMetadataByKey(auth, id, key); + } } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index e865d18f59..636e3a3047 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -49,7 +49,7 @@ export class AuthController { } @Post('validateToken') - @Authenticated() + @Authenticated({ permission: false }) @HttpCode(HttpStatus.OK) validateAccessToken(): ValidateAccessTokenResponseDto { return { authStatus: true }; diff --git a/server/src/controllers/partner.controller.spec.ts b/server/src/controllers/partner.controller.spec.ts new file mode 100644 index 0000000000..2c507a634f --- /dev/null +++ b/server/src/controllers/partner.controller.spec.ts @@ -0,0 +1,101 @@ +import { PartnerController } from 'src/controllers/partner.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PartnerService } from 'src/services/partner.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(PartnerController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(PartnerService); + + beforeAll(async () => { + ctx = await controllerSetup(PartnerController, [ + { provide: PartnerService, useValue: service }, + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /partners', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/partners'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require a direction`, async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'direction should not be empty', + expect.stringContaining('direction must be one of the following values:'), + ]), + ); + }); + + it(`should require direction to be an enum`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get(`/partners`) + .query({ direction: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([expect.stringContaining('direction must be one of the following values:')]), + ); + }); + }); + + describe('POST /partners', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/partners'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require sharedWithId to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post(`/partners`) + .send({ sharedWithId: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); + + describe('PUT /partners/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/partners/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require id to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/partners/invalid`) + .send({ inTimeline: true }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); + + describe('DELETE /partners/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/partners/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require id to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete(`/partners/invalid`) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); +}); diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index f2f4e3d7d6..7cb5c1c274 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -1,7 +1,8 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { EndpointLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { PartnerService } from 'src/services/partner.service'; @@ -18,10 +19,17 @@ export class PartnerController { return this.service.search(auth, dto); } - @Post(':id') + @Post() @Authenticated({ permission: Permission.PartnerCreate }) - createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.create(auth, id); + createPartner(@Auth() auth: AuthDto, @Body() dto: PartnerCreateDto): Promise { + return this.service.create(auth, dto); + } + + @Post(':id') + @EndpointLifecycle({ deprecatedAt: 'v1.141.0' }) + @Authenticated({ permission: Permission.PartnerCreate }) + createPartnerDeprecated(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.create(auth, { sharedWithId: id }); } @Put(':id') @@ -29,7 +37,7 @@ export class PartnerController { updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: UpdatePartnerDto, + @Body() dto: PartnerUpdateDto, ): Promise { return this.service.update(auth, id, dto); } diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index 39d2cb8fcd..adbc8be0f3 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -128,12 +128,6 @@ describe(SearchController.name, () => { await request(ctx.getHttpServer()).post('/search/smart'); expect(ctx.authenticate).toHaveBeenCalled(); }); - - it('should require a query', async () => { - const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({}); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string'])); - }); }); describe('GET /search/explore', () => { diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts new file mode 100644 index 0000000000..bd9c966d42 --- /dev/null +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -0,0 +1,79 @@ +import { UserAdminController } from 'src/controllers/user-admin.controller'; +import { UserAdminCreateDto } from 'src/dtos/user.dto'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { UserAdminService } from 'src/services/user-admin.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(UserAdminController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(UserAdminService); + + beforeAll(async () => { + ctx = await controllerSetup(UserAdminController, [ + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + { provide: UserAdminService, useValue: service }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /admin/users', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/admin/users'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /admin/users', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/admin/users'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should not allow decimal quota`, async () => { + const dto: UserAdminCreateDto = { + email: 'user@immich.app', + password: 'test', + name: 'Test User', + quotaSizeInBytes: 1.2, + }; + + const { status, body } = await request(ctx.getHttpServer()) + .post(`/admin/users`) + .set('Authorization', `Bearer token`) + .send(dto); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + }); + }); + + describe('GET /admin/users/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/admin/users/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /admin/users/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should not allow decimal quota`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/admin/users/${factory.uuid()}`) + .set('Authorization', `Bearer token`) + .send({ quotaSizeInBytes: 1.2 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + }); + }); +}); diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 33a727e75a..96623092f1 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -43,7 +43,9 @@ export class StorageCore { private storageRepository: StorageRepository, private systemMetadataRepository: SystemMetadataRepository, private logger: LoggingRepository, - ) {} + ) { + this.logger.setContext(StorageCore.name); + } static create( assetRepository: AssetRepository, diff --git a/server/src/decorators.ts b/server/src/decorators.ts index b88f2d2d7e..2f1e76d097 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -1,5 +1,5 @@ import { SetMetadata, applyDecorators } from '@nestjs/common'; -import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; +import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger'; import _ from 'lodash'; import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants'; import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum'; @@ -159,12 +159,21 @@ type LifecycleMetadata = { deprecatedAt?: LifecycleRelease; }; -export const EndpointLifecycle = ({ addedAt, deprecatedAt }: LifecycleMetadata) => { +export const EndpointLifecycle = ({ + addedAt, + deprecatedAt, + description, + ...options +}: LifecycleMetadata & ApiOperationOptions) => { const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })]; if (deprecatedAt) { decorators.push( ApiTags('Deprecated'), - ApiOperation({ deprecated: true, description: DEPRECATED_IN_PREFIX + deprecatedAt }), + ApiOperation({ + deprecated: true, + description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''), + ...options, + }), ); } diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index ea86e087d8..755069d827 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,6 +1,8 @@ +import { BadRequestException } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; +import { plainToInstance, Transform, Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto'; import { AssetVisibility } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; @@ -64,6 +66,20 @@ export class AssetMediaCreateDto extends AssetMediaBase { @ValidateUUID({ optional: true }) livePhotoVideoId?: string; + @Transform(({ value }) => { + try { + const json = JSON.parse(value); + const items = Array.isArray(json) ? json : [json]; + return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item)); + } catch { + throw new BadRequestException(['metadata must be valid JSON']); + } + }) + @Optional() + @ValidateNested({ each: true }) + @IsArray() + metadata!: AssetMetadataUpsertItemDto[]; + @ApiProperty({ type: 'string', format: 'binary', required: false }) [UploadFieldName.SIDECAR_DATA]?: any; } diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 31e5679e76..6a89b7e2cf 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -1,21 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { + IsArray, IsDateString, IsInt, IsLatitude, IsLongitude, IsNotEmpty, + IsObject, IsPositive, IsString, IsTimeZone, Max, Min, ValidateIf, + ValidateNested, } from 'class-validator'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetType, AssetVisibility } from 'src/enum'; +import { AssetMetadataKey, AssetType, AssetVisibility } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; +import { AssetMetadata, AssetMetadataItem } from 'src/types'; import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class DeviceIdDto { @@ -135,6 +139,53 @@ export class AssetStatsResponseDto { total!: number; } +export class AssetMetadataRouteParams { + @ValidateUUID() + id!: string; + + @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) + key!: AssetMetadataKey; +} + +export class AssetMetadataUpsertDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetMetadataUpsertItemDto) + items!: AssetMetadataUpsertItemDto[]; +} + +export class AssetMetadataUpsertItemDto implements AssetMetadataItem { + @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) + key!: AssetMetadataKey; + + @IsObject() + @ValidateNested() + @Type((options) => { + switch (options?.object.key) { + case AssetMetadataKey.MobileApp: { + return AssetMetadataMobileAppDto; + } + default: { + return Object; + } + } + }) + value!: AssetMetadata[AssetMetadataKey]; +} + +export class AssetMetadataMobileAppDto { + @IsString() + @Optional() + iCloudId?: string; +} + +export class AssetMetadataResponseDto { + @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) + key!: AssetMetadataKey; + value!: object; + updatedAt!: Date; +} + export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { images: stats[AssetType.Image], diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 28d4adf8b7..599213f662 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,9 +1,14 @@ import { IsNotEmpty } from 'class-validator'; import { UserResponseDto } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; -import { ValidateEnum } from 'src/validation'; +import { ValidateEnum, ValidateUUID } from 'src/validation'; -export class UpdatePartnerDto { +export class PartnerCreateDto { + @ValidateUUID() + sharedWithId!: string; +} + +export class PartnerUpdateDto { @IsNotEmpty() inTimeline!: boolean; } diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index f709ad94ab..5f8b018afe 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -6,7 +6,7 @@ import { PropertyLifecycle } from 'src/decorators'; import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; class BaseSearchDto { @ValidateUUID({ optional: true, nullable: true }) @@ -144,9 +144,7 @@ export class MetadataSearchDto extends RandomSearchDto { @Optional() deviceAssetId?: string; - @IsString() - @IsNotEmpty() - @Optional() + @ValidateString({ optional: true, trim: true }) description?: string; @IsString() @@ -154,9 +152,7 @@ export class MetadataSearchDto extends RandomSearchDto { @Optional() checksum?: string; - @IsString() - @IsNotEmpty() - @Optional() + @ValidateString({ optional: true, trim: true }) originalFileName?: string; @IsString() @@ -190,16 +186,17 @@ export class MetadataSearchDto extends RandomSearchDto { } export class StatisticsSearchDto extends BaseSearchDto { - @IsString() - @IsNotEmpty() - @Optional() + @ValidateString({ optional: true, trim: true }) description?: string; } export class SmartSearchDto extends BaseSearchWithResultsDto { - @IsString() - @IsNotEmpty() - query!: string; + @ValidateString({ optional: true, trim: true }) + query?: string; + + @ValidateUUID({ optional: true }) + @Optional() + queryAssetId?: string; @IsString() @IsNotEmpty() diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 9ac85755ab..c936ec52cc 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -4,6 +4,7 @@ import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AlbumUserRole, + AssetMetadataKey, AssetOrder, AssetType, AssetVisibility, @@ -162,6 +163,21 @@ export class SyncAssetExifV1 { fps!: number | null; } +@ExtraModel() +export class SyncAssetMetadataV1 { + assetId!: string; + @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) + key!: AssetMetadataKey; + value!: object; +} + +@ExtraModel() +export class SyncAssetMetadataDeleteV1 { + assetId!: string; + @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) + key!: AssetMetadataKey; +} + @ExtraModel() export class SyncAlbumDeleteV1 { albumId!: string; @@ -320,6 +336,9 @@ export class SyncAckV1 {} @ExtraModel() export class SyncResetV1 {} +@ExtraModel() +export class SyncCompleteV1 {} + export type SyncItem = { [SyncEntityType.AuthUserV1]: SyncAuthUserV1; [SyncEntityType.UserV1]: SyncUserV1; @@ -328,6 +347,8 @@ export type SyncItem = { [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; [SyncEntityType.AssetV1]: SyncAssetV1; [SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1; + [SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1; + [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; [SyncEntityType.AssetExifV1]: SyncAssetExifV1; [SyncEntityType.PartnerAssetV1]: SyncAssetV1; [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; @@ -364,6 +385,7 @@ export type SyncItem = { [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; [SyncEntityType.SyncAckV1]: SyncAckV1; + [SyncEntityType.SyncCompleteV1]: SyncCompleteV1; [SyncEntityType.SyncResetV1]: SyncResetV1; }; diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 8a58995de7..1facc6c331 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Exclude, Transform, Type } from 'class-transformer'; +import { Type } from 'class-transformer'; import { ArrayMinSize, IsInt, @@ -15,7 +15,6 @@ import { ValidateNested, } from 'class-validator'; import { SystemConfig } from 'src/config'; -import { PropertyLifecycle } from 'src/decorators'; import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto'; import { AudioCodec, @@ -257,21 +256,32 @@ class SystemConfigLoggingDto { level!: LogLevel; } +class MachineLearningAvailabilityChecksDto { + @ValidateBoolean() + enabled!: boolean; + + @IsInt() + timeout!: number; + + @IsInt() + interval!: number; +} + class SystemConfigMachineLearningDto { @ValidateBoolean() enabled!: boolean; - @PropertyLifecycle({ deprecatedAt: 'v1.122.0' }) - @Exclude() - url?: string; - @IsUrl({ require_tld: false, allow_underscores: true }, { each: true }) @ArrayMinSize(1) - @Transform(({ obj, value }) => (obj.url ? [obj.url] : value)) @ValidateIf((dto) => dto.enabled) @ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 }) urls!: string[]; + @Type(() => MachineLearningAvailabilityChecksDto) + @ValidateNested() + @IsObject() + availabilityChecks!: MachineLearningAvailabilityChecksDto; + @Type(() => CLIPConfig) @ValidateNested() @IsObject() diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 449cec3207..58772da00b 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -53,6 +53,12 @@ export class TimeBucketDto { description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', }) visibility?: AssetVisibility; + + @ValidateBoolean({ + optional: true, + description: 'Include location data in the response', + }) + withCoordinates?: boolean; } export class TimeBucketAssetDto extends TimeBucketDto { @@ -185,6 +191,22 @@ export class TimeBucketAssetResponseDto { description: 'Array of country names extracted from EXIF GPS data', }) country!: (string | null)[]; + + @ApiProperty({ + type: 'array', + required: false, + items: { type: 'number', nullable: true }, + description: 'Array of latitude coordinates extracted from EXIF GPS data', + }) + latitude!: number[]; + + @ApiProperty({ + type: 'array', + required: false, + items: { type: 'number', nullable: true }, + description: 'Array of longitude coordinates extracted from EXIF GPS data', + }) + longitude!: number[]; } export class TimeBucketsResponseDto { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 0da86bfcb5..443178aa10 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; +import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; import { User, UserAdmin } from 'src/database'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserMetadataItem } from 'src/types'; @@ -91,7 +91,7 @@ export class UserAdminCreateDto { storageLabel?: string | null; @Optional({ nullable: true }) - @IsNumber() + @IsInt() @Min(0) @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; @@ -137,7 +137,7 @@ export class UserAdminUpdateDto { shouldChangePassword?: boolean; @Optional({ nullable: true }) - @IsNumber() + @IsInt() @Min(0) @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 3bed3a5b36..6fd2abb055 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -29,8 +29,8 @@ export const AlbumUpdateEmail = ({ - New media has been added to {albumName}, -
check it out! + New media has been added to {albumName}. +
Check it out!
); diff --git a/server/src/emails/components/button.component.tsx b/server/src/emails/components/button.component.tsx index b490e36650..a1fc4636cc 100644 --- a/server/src/emails/components/button.component.tsx +++ b/server/src/emails/components/button.component.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Button, ButtonProps } from '@react-email/components'; +import { Button, ButtonProps, Text } from '@react-email/components'; export const ImmichButton = ({ children, ...props }: ButtonProps) => ( ); diff --git a/server/src/enum.ts b/server/src/enum.ts index 65e102dbd6..52190c79bc 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -277,6 +277,10 @@ export enum UserMetadataKey { Onboarding = 'onboarding', } +export enum AssetMetadataKey { + MobileApp = 'mobile-app', +} + export enum UserAvatarColor { Primary = 'primary', Pink = 'pink', @@ -527,6 +531,7 @@ export enum JobName { AssetGenerateThumbnails = 'AssetGenerateThumbnails', AuditLogCleanup = 'AuditLogCleanup', + AuditTableCleanup = 'AuditTableCleanup', DatabaseBackup = 'DatabaseBackup', @@ -627,6 +632,7 @@ export enum SyncRequestType { AlbumAssetExifsV1 = 'AlbumAssetExifsV1', AssetsV1 = 'AssetsV1', AssetExifsV1 = 'AssetExifsV1', + AssetMetadataV1 = 'AssetMetadataV1', AuthUsersV1 = 'AuthUsersV1', MemoriesV1 = 'MemoriesV1', MemoryToAssetsV1 = 'MemoryToAssetsV1', @@ -650,6 +656,8 @@ export enum SyncEntityType { AssetV1 = 'AssetV1', AssetDeleteV1 = 'AssetDeleteV1', AssetExifV1 = 'AssetExifV1', + AssetMetadataV1 = 'AssetMetadataV1', + AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', PartnerV1 = 'PartnerV1', PartnerDeleteV1 = 'PartnerDeleteV1', @@ -701,6 +709,7 @@ export enum SyncEntityType { SyncAckV1 = 'SyncAckV1', SyncResetV1 = 'SyncResetV1', + SyncCompleteV1 = 'SyncCompleteV1', } export enum NotificationLevel { diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 59c28849e1..6dfd11ee4b 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -12,7 +12,7 @@ import { AuthRequest } from 'src/middleware/auth.guard'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { AssetMediaService } from 'src/services/asset-media.service'; import { ImmichFile, UploadFile, UploadFiles } from 'src/types'; -import { asRequest, mapToUploadFile } from 'src/utils/asset.util'; +import { asUploadRequest, mapToUploadFile } from 'src/utils/asset.util'; export function getFile(files: UploadFiles, property: 'assetData' | 'sidecarData') { const file = files[property]?.[0]; @@ -99,18 +99,21 @@ export class FileUploadInterceptor implements NestInterceptor { } private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) { - return callbackify(() => this.assetService.canUploadFile(asRequest(request, file)), callback); + return callbackify(() => this.assetService.canUploadFile(asUploadRequest(request, file)), callback); } private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { return callbackify( - () => this.assetService.getUploadFilename(asRequest(request, file)), + () => this.assetService.getUploadFilename(asUploadRequest(request, file)), callback as Callback, ); } private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { - return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback); + return callbackify( + () => this.assetService.getUploadFolder(asUploadRequest(request, file)), + callback as Callback, + ); } private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback>) { diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index d4418edc56..4ab809bc18 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -547,9 +547,8 @@ where "asset"."visibility" != $1 and "asset"."deletedAt" is null and "job_status"."previewAt" is not null - and "job_status"."facesRecognizedAt" is null order by - "asset"."createdAt" desc + "asset"."fileCreatedAt" desc -- AssetJobRepository.streamForMigrationJob select diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index e2bc80eabe..1283ff0a66 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -19,6 +19,33 @@ returning "dateTimeOriginal", "timeZone" +-- AssetRepository.getMetadata +select + "key", + "value", + "updatedAt" +from + "asset_metadata" +where + "assetId" = $1 + +-- AssetRepository.getMetadataByKey +select + "key", + "value", + "updatedAt" +from + "asset_metadata" +where + "assetId" = $1 + and "key" = $2 + +-- AssetRepository.deleteMetadataByKey +delete from "asset_metadata" +where + "assetId" = $1 + and "key" = $2 + -- AssetRepository.getByDayOfYear with "res" as ( diff --git a/server/src/queries/duplicate.repository.sql b/server/src/queries/duplicate.repository.sql index 8913007dea..3f718f84c2 100644 --- a/server/src/queries/duplicate.repository.sql +++ b/server/src/queries/duplicate.repository.sql @@ -12,7 +12,7 @@ with ) as "assets" from "asset" - left join lateral ( + inner join lateral ( select "asset".*, "asset_exif" as "exifInfo" diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index be2245a74e..e0aaedfdf3 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -123,6 +123,14 @@ offset $8 commit +-- SearchRepository.getEmbedding +select + * +from + "smart_search" +where + "assetId" = $1 + -- SearchRepository.searchFaces begin set diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 80021368a0..809b59df10 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -539,6 +539,37 @@ where order by "asset_face"."updateId" asc +-- SyncRepository.assetMetadata.getDeletes +select + "asset_metadata_audit"."id", + "assetId", + "key" +from + "asset_metadata_audit" as "asset_metadata_audit" + left join "asset" on "asset"."id" = "asset_metadata_audit"."assetId" +where + "asset_metadata_audit"."id" < $1 + and "asset_metadata_audit"."id" > $2 + and "asset"."ownerId" = $3 +order by + "asset_metadata_audit"."id" asc + +-- SyncRepository.assetMetadata.getUpserts +select + "assetId", + "key", + "value", + "asset_metadata"."updateId" +from + "asset_metadata" as "asset_metadata" + inner join "asset" on "asset"."id" = "asset_metadata"."assetId" +where + "asset_metadata"."updateId" < $1 + and "asset_metadata"."updateId" > $2 + and "asset"."ownerId" = $3 +order by + "asset_metadata"."updateId" asc + -- SyncRepository.authUser.getUpserts select "id", @@ -560,6 +591,7 @@ from where "user"."updateId" < $1 and "user"."updateId" > $2 + and "id" = $3 order by "user"."updateId" asc @@ -926,7 +958,7 @@ where order by "stack"."updateId" asc --- SyncRepository.people.getDeletes +-- SyncRepository.person.getDeletes select "id", "personId" @@ -939,7 +971,7 @@ where order by "person_audit"."id" asc --- SyncRepository.people.getUpserts +-- SyncRepository.person.getUpserts select "id", "createdAt", diff --git a/server/src/queries/view.repository.sql b/server/src/queries/view.repository.sql index 81f5ca20b8..31da10123f 100644 --- a/server/src/queries/view.repository.sql +++ b/server/src/queries/view.repository.sql @@ -12,6 +12,8 @@ where and "fileCreatedAt" is not null and "fileModifiedAt" is not null and "localDateTime" is not null +order by + "directoryPath" asc -- ViewRepository.getAssetsByOriginalPath select diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index d32da27ff0..f4b8899341 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -60,7 +60,7 @@ export class AssetJobRepository { .selectFrom('asset') .where('asset.id', '=', asUuid(id)) .select(['id', 'originalPath']) - .select(withFiles) + .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .limit(1) .executeTakeFirst(); } @@ -360,9 +360,9 @@ export class AssetJobRepository { @GenerateSql({ params: [], stream: true }) streamForDetectFacesJob(force?: boolean) { return this.assetsWithPreviews() - .$if(!force, (qb) => qb.where('job_status.facesRecognizedAt', 'is', null)) + .$if(force === false, (qb) => qb.where('job_status.facesRecognizedAt', 'is', null)) .select(['asset.id']) - .orderBy('asset.createdAt', 'desc') + .orderBy('asset.fileCreatedAt', 'desc') .stream(); } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 482b944a80..8634b629c5 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,15 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely'; +import { Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { Stack } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; -import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; +import { AssetMetadataItem } from 'src/types'; import { anyUuid, asUuid, @@ -59,6 +60,7 @@ interface AssetBuilderOptions { status?: AssetStatus; assetType?: AssetType; visibility?: AssetVisibility; + withCoordinates?: boolean; } export interface TimeBucketOptions extends AssetBuilderOptions { @@ -210,6 +212,43 @@ export class AssetRepository { .execute(); } + @GenerateSql({ params: [DummyValue.UUID] }) + getMetadata(assetId: string) { + return this.db + .selectFrom('asset_metadata') + .select(['key', 'value', 'updatedAt']) + .where('assetId', '=', assetId) + .execute(); + } + + upsertMetadata(id: string, items: AssetMetadataItem[]) { + return this.db + .insertInto('asset_metadata') + .values(items.map((item) => ({ assetId: id, ...item }))) + .onConflict((oc) => + oc + .columns(['assetId', 'key']) + .doUpdateSet((eb) => ({ key: eb.ref('excluded.key'), value: eb.ref('excluded.value') })), + ) + .returning(['key', 'value', 'updatedAt']) + .execute(); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + getMetadataByKey(assetId: string, key: AssetMetadataKey) { + return this.db + .selectFrom('asset_metadata') + .select(['key', 'value', 'updatedAt']) + .where('assetId', '=', assetId) + .where('key', '=', key) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async deleteMetadataByKey(id: string, key: AssetMetadataKey) { + await this.db.deleteFrom('asset_metadata').where('assetId', '=', id).where('key', '=', key).execute(); + } + create(asset: Insertable) { return this.db.insertInto('asset').values(asset).returningAll().executeTakeFirstOrThrow(); } @@ -590,6 +629,7 @@ export class AssetRepository { ) .as('ratio'), ]) + .$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude'])) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility == undefined, withDefaultVisibility) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) @@ -663,6 +703,12 @@ export class AssetRepository { 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.withCoordinates, (qb) => + qb.select((eb) => [ + eb.fn.coalesce(eb.fn('array_agg', ['latitude']), sql.lit('{}')).as('latitude'), + eb.fn.coalesce(eb.fn('array_agg', ['longitude']), sql.lit('{}')).as('longitude'), + ]), + ) .$if(!!options.withStacked, (qb) => qb.select((eb) => eb.fn.coalesce(eb.fn('json_agg', ['stack']), sql.lit('[]')).as('stack')), ), diff --git a/server/src/repositories/duplicate.repository.ts b/server/src/repositories/duplicate.repository.ts index 140c42a643..95ccbea63d 100644 --- a/server/src/repositories/duplicate.repository.ts +++ b/server/src/repositories/duplicate.repository.ts @@ -34,7 +34,7 @@ export class DuplicateRepository { qb .selectFrom('asset') .$call(withDefaultVisibility) - .leftJoinLateral( + .innerJoinLateral( (qb) => qb .selectFrom('asset_exif') diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index c1b26d5dde..ec4c8a8f52 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -81,7 +81,7 @@ type EventMap = { StackDeleteAll: [{ stackIds: string[]; userId: string }]; // user events - UserSignup: [{ notify: boolean; id: string; tempPassword?: string }]; + UserSignup: [{ notify: boolean; id: string; password?: string }]; // websocket events WebsocketConnect: [{ userId: string }]; diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 939ecb718f..576ee6c810 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -142,6 +142,10 @@ export class LoggingRepository { this.handleMessage(LogLevel.Fatal, message, details); } + deprecate(message: string) { + this.warn(`[Deprecated] ${message}`); + } + private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) { if (this.logger.isLevelEnabled(level)) { this.handleMessage(level, message(), details); diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index f880ed1298..d148dc782b 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { Duration } from 'luxon'; import { readFile } from 'node:fs/promises'; -import { MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME, MACHINE_LEARNING_PING_TIMEOUT } from 'src/constants'; +import { MachineLearningConfig } from 'src/config'; import { CLIPConfig } from 'src/dtos/model-config.dto'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -57,82 +58,100 @@ export type TextEncodingOptions = ModelOptions & { language?: string }; @Injectable() export class MachineLearningRepository { - // Note that deleted URL's are not removed from this map (ie: they're leaked) - // Cleaning them up is low priority since there should be very few over a - // typical server uptime cycle - private urlAvailability: { - [url: string]: - | { - active: boolean; - lastChecked: number; - } - | undefined; - }; + private healthyMap: Record = {}; + private interval?: ReturnType; + private _config?: MachineLearningConfig; + + private get config(): MachineLearningConfig { + if (!this._config) { + throw new Error('Machine learning repository not been setup'); + } + + return this._config; + } constructor(private logger: LoggingRepository) { this.logger.setContext(MachineLearningRepository.name); - this.urlAvailability = {}; } - private setUrlAvailability(url: string, active: boolean) { - const current = this.urlAvailability[url]; - if (current?.active !== active) { - this.logger.verbose(`Setting ${url} ML server to ${active ? 'active' : 'inactive'}.`); + setup(config: MachineLearningConfig) { + this._config = config; + this.teardown(); + + // delete old servers + for (const url of Object.keys(this.healthyMap)) { + if (!config.urls.includes(url)) { + delete this.healthyMap[url]; + } } - this.urlAvailability[url] = { - active, - lastChecked: Date.now(), - }; + + if (!config.availabilityChecks.enabled) { + return; + } + + this.tick(); + this.interval = setInterval( + () => this.tick(), + Duration.fromObject({ milliseconds: config.availabilityChecks.interval }).as('milliseconds'), + ); } - private async checkAvailability(url: string) { - let active = false; + teardown() { + if (this.interval) { + clearInterval(this.interval); + } + } + + private tick() { + for (const url of this.config.urls) { + void this.check(url); + } + } + + private async check(url: string) { + let healthy = false; try { const response = await fetch(new URL('/ping', url), { - signal: AbortSignal.timeout(MACHINE_LEARNING_PING_TIMEOUT), + signal: AbortSignal.timeout(this.config.availabilityChecks.timeout), }); - active = response.ok; + if (response.ok) { + healthy = true; + } } catch { // nothing to do here } - this.setUrlAvailability(url, active); - return active; + + this.setHealthy(url, healthy); } - private async shouldSkipUrl(url: string) { - const availability = this.urlAvailability[url]; - if (availability === undefined) { - // If this is a new endpoint, then check inline and skip if it fails - if (!(await this.checkAvailability(url))) { - return true; - } - return false; + private setHealthy(url: string, healthy: boolean) { + if (this.healthyMap[url] !== healthy) { + this.logger.log(`Machine learning server became ${healthy ? 'healthy' : 'unhealthy'} (${url}).`); } - if (!availability.active && Date.now() - availability.lastChecked < MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME) { - // If this is an old inactive endpoint that hasn't been checked in a - // while then check but don't wait for the result, just skip it - // This avoids delays on every search whilst allowing higher priority - // ML servers to recover over time. - void this.checkAvailability(url); + + this.healthyMap[url] = healthy; + } + + private isHealthy(url: string) { + if (!this.config.availabilityChecks.enabled) { return true; } - return false; + + return this.healthyMap[url]; } - private async predict(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise { + private async predict(payload: ModelPayload, config: MachineLearningRequest): Promise { const formData = await this.getFormData(payload, config); - let urlCounter = 0; - for (const url of urls) { - urlCounter++; - const isLast = urlCounter >= urls.length; - if (!isLast && (await this.shouldSkipUrl(url))) { - continue; - } + for (const url of [ + // try healthy servers first + ...this.config.urls.filter((url) => this.isHealthy(url)), + ...this.config.urls.filter((url) => !this.isHealthy(url)), + ]) { try { const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData }); if (response.ok) { - this.setUrlAvailability(url, true); + this.setHealthy(url, true); return response.json(); } @@ -144,20 +163,21 @@ export class MachineLearningRepository { `Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`, ); } - this.setUrlAvailability(url, false); + + this.setHealthy(url, false); } throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`); } - async detectFaces(urls: string[], imagePath: string, { modelName, minScore }: FaceDetectionOptions) { + async detectFaces(imagePath: string, { modelName, minScore }: FaceDetectionOptions) { const request = { [ModelTask.FACIAL_RECOGNITION]: { [ModelType.DETECTION]: { modelName, options: { minScore } }, [ModelType.RECOGNITION]: { modelName }, }, }; - const response = await this.predict(urls, { imagePath }, request); + const response = await this.predict({ imagePath }, request); return { imageHeight: response.imageHeight, imageWidth: response.imageWidth, @@ -165,15 +185,15 @@ export class MachineLearningRepository { }; } - async encodeImage(urls: string[], imagePath: string, { modelName }: CLIPConfig) { + async encodeImage(imagePath: string, { modelName }: CLIPConfig) { const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } }; - const response = await this.predict(urls, { imagePath }, request); + const response = await this.predict({ imagePath }, request); return response[ModelTask.SEARCH]; } - async encodeText(urls: string[], text: string, { language, modelName }: TextEncodingOptions) { + async encodeText(text: string, { language, modelName }: TextEncodingOptions) { const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName, options: { language } } } }; - const response = await this.predict(urls, { text }, request); + const response = await this.predict({ text }, request); return response[ModelTask.SEARCH]; } diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 6266acf0ed..fa36c41798 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -57,28 +57,28 @@ export class MediaRepository { const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input); return { buffer, format: RawExtractedFormat.Jpeg }; } catch (error: any) { - this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message); + this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`); } try { const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input); return { buffer, format: RawExtractedFormat.Jpeg }; } catch (error: any) { - this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message); + this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`); } try { const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input); return { buffer, format: RawExtractedFormat.Jxl }; } catch (error: any) { - this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message); + this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`); } try { const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input); return { buffer, format: RawExtractedFormat.Jpeg }; } catch (error: any) { - this.logger.debug('Could not extract preview buffer from image', error.message); + this.logger.debug(`Could not extract preview buffer from image: ${error}`); return null; } } @@ -141,6 +141,7 @@ export class MediaRepository { failOn: options.processInvalidImages ? 'none' : 'error', limitInputPixels: false, raw: options.raw, + unlimited: true, }) .pipelineColorspace(options.colorspace === Colorspace.Srgb ? 'srgb' : 'rgb16') .withIccProfile(options.colorspace); diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index bf3a96f21f..e2360156e4 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -103,7 +103,7 @@ export class MetadataRepository { readTags(path: string): Promise { return this.exiftool.read(path).catch((error) => { - this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack); + this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`); return {}; }) as Promise; } diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index 9a436e4b9a..58b1144647 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -29,6 +29,7 @@ export class OAuthRepository { ); const client = await this.getClient(config); state ??= randomState(); + let codeVerifier: string | null; if (codeChallenge) { codeVerifier = null; @@ -36,13 +37,20 @@ export class OAuthRepository { codeVerifier = randomPKCECodeVerifier(); codeChallenge = await calculatePKCECodeChallenge(codeVerifier); } - const url = buildAuthorizationUrl(client, { + + const params: Record = { redirect_uri: redirectUrl, scope: config.scope, state, - code_challenge: client.serverMetadata().supportsPKCE() ? codeChallenge : '', - code_challenge_method: client.serverMetadata().supportsPKCE() ? 'S256' : '', - }).toString(); + }; + + if (client.serverMetadata().supportsPKCE()) { + params.code_challenge = codeChallenge; + params.code_challenge_method = 'S256'; + } + + const url = buildAuthorizationUrl(client, params).toString(); + return { url, state, codeVerifier }; } diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 36ef7a27f1..88de2fb06f 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -293,6 +293,13 @@ export class SearchRepository { }); } + @GenerateSql({ + params: [DummyValue.UUID], + }) + async getEmbedding(assetId: string) { + return this.db.selectFrom('smart_search').selectAll().where('assetId', '=', assetId).executeTakeFirst(); + } + @GenerateSql({ params: [ { diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 13e933fd2f..d8be720f45 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Kysely } from 'kysely'; +import { Kysely, sql } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -54,6 +54,7 @@ export class SyncRepository { asset: AssetSync; assetExif: AssetExifSync; assetFace: AssetFaceSync; + assetMetadata: AssetMetadataSync; authUser: AuthUserSync; memory: MemorySync; memoryToAsset: MemoryToAssetSync; @@ -61,7 +62,7 @@ export class SyncRepository { partnerAsset: PartnerAssetsSync; partnerAssetExif: PartnerAssetExifsSync; partnerStack: PartnerStackSync; - people: PersonSync; + person: PersonSync; stack: StackSync; user: UserSync; userMetadata: UserMetadataSync; @@ -75,6 +76,7 @@ export class SyncRepository { this.asset = new AssetSync(this.db); this.assetExif = new AssetExifSync(this.db); this.assetFace = new AssetFaceSync(this.db); + this.assetMetadata = new AssetMetadataSync(this.db); this.authUser = new AuthUserSync(this.db); this.memory = new MemorySync(this.db); this.memoryToAsset = new MemoryToAssetSync(this.db); @@ -82,7 +84,7 @@ export class SyncRepository { this.partnerAsset = new PartnerAssetsSync(this.db); this.partnerAssetExif = new PartnerAssetExifsSync(this.db); this.partnerStack = new PartnerStackSync(this.db); - this.people = new PersonSync(this.db); + this.person = new PersonSync(this.db); this.stack = new StackSync(this.db); this.user = new UserSync(this.db); this.userMetadata = new UserMetadataSync(this.db); @@ -115,6 +117,15 @@ class BaseSync { .orderBy(idRef, 'asc'); } + protected auditCleanup(t: T, days: number) { + const { table, ref } = this.db.dynamic; + + return this.db + .deleteFrom(table(t).as(t)) + .where(ref(`${t}.deletedAt`), '<', sql.raw(`now() - interval '${days} days'`)) + .execute(); + } + protected upsertQuery(t: T, { nowId, ack }: SyncQueryOptions) { const { table, ref } = this.db.dynamic; const updateIdRef = ref(`${t}.updateId`); @@ -148,6 +159,10 @@ class AlbumSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('album_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -284,6 +299,10 @@ class AlbumToAssetSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('album_asset_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -332,6 +351,10 @@ class AlbumUserSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('album_user_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -369,6 +392,10 @@ class AssetSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('asset', options) @@ -385,6 +412,7 @@ class AuthUserSync extends BaseSync { return this.upsertQuery('user', options) .select(columns.syncUser) .select(['isAdmin', 'pinCode', 'oauthId', 'storageLabel', 'quotaSizeInBytes', 'quotaUsageInBytes']) + .where('id', '=', options.userId) .stream(); } } @@ -398,6 +426,10 @@ class PersonSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('person_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('person', options) @@ -429,6 +461,10 @@ class AssetFaceSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_face_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('asset_face', options) @@ -471,6 +507,10 @@ class MemorySync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('memory_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('memory', options) @@ -503,6 +543,10 @@ class MemoryToAssetSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('memory_asset_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('memory_asset', options) @@ -535,6 +579,10 @@ class PartnerSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('partner_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -614,6 +662,10 @@ class StackSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('stack_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('stack', options) @@ -662,6 +714,10 @@ class UserSync extends BaseSync { return this.auditQuery('user_audit', options).select(['id', 'userId']).stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('user_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('user', options).select(columns.syncUser).stream(); @@ -677,6 +733,10 @@ class UserMetadataSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('user_metadata_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('user_metadata', options) @@ -685,3 +745,27 @@ class UserMetadataSync extends BaseSync { .stream(); } } + +class AssetMetadataSync extends BaseSync { + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) + getDeletes(options: SyncQueryOptions, userId: string) { + return this.auditQuery('asset_metadata_audit', options) + .select(['asset_metadata_audit.id', 'assetId', 'key']) + .leftJoin('asset', 'asset.id', 'asset_metadata_audit.assetId') + .where('asset.ownerId', '=', userId) + .stream(); + } + + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_metadata_audit', daysAgo); + } + + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) + getUpserts(options: SyncQueryOptions, userId: string) { + return this.upsertQuery('asset_metadata', options) + .select(['assetId', 'key', 'value', 'asset_metadata.updateId']) + .innerJoin('asset', 'asset.id', 'asset_metadata.assetId') + .where('asset.ownerId', '=', userId) + .stream(); + } +} diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index a63a4cc553..44f4a2bb9c 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -7,13 +7,10 @@ import { columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetType, AssetVisibility, UserStatus } from 'src/enum'; import { DB } from 'src/schema'; -import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; import { UserTable } from 'src/schema/tables/user.table'; import { UserMetadata, UserMetadataItem } from 'src/types'; import { asUuid } from 'src/utils/database'; -type Upsert = Insertable; - export interface UserListFilter { id?: string; withDeleted?: boolean; @@ -211,12 +208,12 @@ export class UserRepository { async upsertMetadata(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { await this.db .insertInto('user_metadata') - .values({ userId: id, key, value } as Upsert) + .values({ userId: id, key, value }) .onConflict((oc) => oc.columns(['userId', 'key']).doUpdateSet({ key, value, - } as Upsert), + }), ) .execute(); } diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts index 93c1280191..ceab79f6eb 100644 --- a/server/src/repositories/view-repository.ts +++ b/server/src/repositories/view-repository.ts @@ -20,6 +20,7 @@ export class ViewRepository { .where('fileCreatedAt', 'is not', null) .where('fileModifiedAt', 'is not', null) .where('localDateTime', 'is not', null) + .orderBy('directoryPath', 'asc') .execute(); return results.map((row) => row.directoryPath.replaceAll(/\/$/g, '')); diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 786e7a1ffa..e255742b5d 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -230,6 +230,19 @@ export const user_metadata_audit = registerFunction({ END`, }); +export const asset_metadata_audit = registerFunction({ + name: 'asset_metadata_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO asset_metadata_audit ("assetId", "key") + SELECT "assetId", "key" + FROM OLD; + RETURN NULL; + END`, +}); + export const asset_face_audit = registerFunction({ name: 'asset_face_audit', returnType: 'TRIGGER', diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 8982437b34..c8474cda03 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -5,6 +5,7 @@ import { album_user_delete_audit, asset_delete_audit, asset_face_audit, + asset_metadata_audit, f_concat_ws, f_unaccent, immich_uuid_v7, @@ -32,6 +33,8 @@ import { AssetFaceAuditTable } from 'src/schema/tables/asset-face-audit.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; +import { AssetMetadataAuditTable } from 'src/schema/tables/asset-metadata-audit.table'; +import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; import { AssetTable } from 'src/schema/tables/asset.table'; import { AuditTable } from 'src/schema/tables/audit.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; @@ -81,6 +84,8 @@ export class ImmichDatabase { AssetAuditTable, AssetFaceTable, AssetFaceAuditTable, + AssetMetadataTable, + AssetMetadataAuditTable, AssetJobStatusTable, AssetTable, AssetFileTable, @@ -135,6 +140,7 @@ export class ImmichDatabase { stack_delete_audit, person_delete_audit, user_metadata_audit, + asset_metadata_audit, asset_face_audit, ]; @@ -160,12 +166,14 @@ export interface DB { api_key: ApiKeyTable; asset: AssetTable; + asset_audit: AssetAuditTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; asset_face_audit: AssetFaceAuditTable; asset_file: AssetFileTable; + asset_metadata: AssetMetadataTable; + asset_metadata_audit: AssetMetadataAuditTable; asset_job_status: AssetJobStatusTable; - asset_audit: AssetAuditTable; audit: AuditTable; diff --git a/server/src/schema/migrations/1756318797207-AssetMetadataTables.ts b/server/src/schema/migrations/1756318797207-AssetMetadataTables.ts new file mode 100644 index 0000000000..ba0bad9d9a --- /dev/null +++ b/server/src/schema/migrations/1756318797207-AssetMetadataTables.ts @@ -0,0 +1,58 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_metadata_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO asset_metadata_audit ("assetId", "key") + SELECT "assetId", "key" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "asset_metadata_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "assetId" uuid NOT NULL, + "key" character varying NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "asset_metadata_audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "asset_metadata_audit_assetId_idx" ON "asset_metadata_audit" ("assetId");`.execute(db); + await sql`CREATE INDEX "asset_metadata_audit_key_idx" ON "asset_metadata_audit" ("key");`.execute(db); + await sql`CREATE INDEX "asset_metadata_audit_deletedAt_idx" ON "asset_metadata_audit" ("deletedAt");`.execute(db); + await sql`CREATE TABLE "asset_metadata" ( + "assetId" uuid NOT NULL, + "key" character varying NOT NULL, + "value" jsonb NOT NULL, + "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), + "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT "asset_metadata_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "asset" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "asset_metadata_pkey" PRIMARY KEY ("assetId", "key") +);`.execute(db); + await sql`CREATE INDEX "asset_metadata_updateId_idx" ON "asset_metadata" ("updateId");`.execute(db); + await sql`CREATE INDEX "asset_metadata_updatedAt_idx" ON "asset_metadata" ("updatedAt");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_metadata_audit" + AFTER DELETE ON "asset_metadata" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_metadata_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_metadata_updated_at" + BEFORE UPDATE ON "asset_metadata" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_metadata_audit', '{"type":"function","name":"asset_metadata_audit","sql":"CREATE OR REPLACE FUNCTION asset_metadata_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_metadata_audit (\\"assetId\\", \\"key\\")\\n SELECT \\"assetId\\", \\"key\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_metadata_audit', '{"type":"trigger","name":"asset_metadata_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_metadata_audit\\"\\n AFTER DELETE ON \\"asset_metadata\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_metadata_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_metadata_updated_at', '{"type":"trigger","name":"asset_metadata_updated_at","sql":"CREATE OR REPLACE TRIGGER \\"asset_metadata_updated_at\\"\\n BEFORE UPDATE ON \\"asset_metadata\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TABLE "asset_metadata_audit";`.execute(db); + await sql`DROP TABLE "asset_metadata";`.execute(db); + await sql`DROP FUNCTION asset_metadata_audit;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_metadata_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_metadata_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_metadata_updated_at';`.execute(db); +} diff --git a/server/src/schema/tables/asset-metadata-audit.table.ts b/server/src/schema/tables/asset-metadata-audit.table.ts new file mode 100644 index 0000000000..3b94ce6d1a --- /dev/null +++ b/server/src/schema/tables/asset-metadata-audit.table.ts @@ -0,0 +1,18 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { AssetMetadataKey } from 'src/enum'; +import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; + +@Table('asset_metadata_audit') +export class AssetMetadataAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', index: true }) + assetId!: string; + + @Column({ index: true }) + key!: AssetMetadataKey; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/asset-metadata.table.ts b/server/src/schema/tables/asset-metadata.table.ts new file mode 100644 index 0000000000..486101408d --- /dev/null +++ b/server/src/schema/tables/asset-metadata.table.ts @@ -0,0 +1,46 @@ +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetMetadataKey } from 'src/enum'; +import { asset_metadata_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { + AfterDeleteTrigger, + Column, + ForeignKeyColumn, + Generated, + PrimaryColumn, + Table, + Timestamp, + UpdateDateColumn, +} from 'src/sql-tools'; +import { AssetMetadata, AssetMetadataItem } from 'src/types'; + +@UpdatedAtTrigger('asset_metadata_updated_at') +@Table('asset_metadata') +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_metadata_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) +export class AssetMetadataTable implements AssetMetadataItem { + @ForeignKeyColumn(() => AssetTable, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + primary: true, + // [assetId, key] is the PK constraint + index: false, + }) + assetId!: string; + + @PrimaryColumn({ type: 'character varying' }) + key!: T; + + @Column({ type: 'jsonb' }) + value!: AssetMetadata[T]; + + @UpdateIdColumn({ index: true }) + updateId!: Generated; + + @UpdateDateColumn({ index: true }) + updatedAt!: Generated; +} diff --git a/server/src/schema/tables/memory-asset-audit.table.ts b/server/src/schema/tables/memory-asset-audit.table.ts index 77a889b455..218c2f19ff 100644 --- a/server/src/schema/tables/memory-asset-audit.table.ts +++ b/server/src/schema/tables/memory-asset-audit.table.ts @@ -1,11 +1,11 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { MemoryTable } from 'src/schema/tables/memory.table'; -import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('memory_asset_audit') export class MemoryAssetAuditTable { @PrimaryGeneratedUuidV7Column() - id!: string; + id!: Generated; @ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' }) memoryId!: string; @@ -14,5 +14,5 @@ export class MemoryAssetAuditTable { assetId!: string; @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) - deletedAt!: Date; + deletedAt!: Generated; } diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index eb34f142c6..267261bc82 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -25,6 +25,7 @@ const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); const uploadFile = { nullAuth: { auth: null, + body: {}, fieldName: UploadFieldName.ASSET_DATA, file: { uuid: 'random-uuid', @@ -37,6 +38,7 @@ const uploadFile = { filename: (fieldName: UploadFieldName, filename: string) => { return { auth: authStub.admin, + body: {}, fieldName, file: { uuid: 'random-uuid', @@ -916,7 +918,10 @@ describe(AssetMediaService.name, () => { describe('onUploadError', () => { it('should queue a job to delete the uploaded file', async () => { - const request = { user: authStub.user1 } as AuthRequest; + const request = { + body: {}, + user: authStub.user1, + } as AuthRequest; const file = { fieldname: UploadFieldName.ASSET_DATA, diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 9ae200a2ec..d1bd2d1327 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -33,20 +33,14 @@ import { } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { BaseService } from 'src/services/base.service'; -import { UploadFile } from 'src/types'; +import { UploadFile, UploadRequest } from 'src/types'; import { requireUploadAccess } from 'src/utils/access'; -import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; -import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; +import { asUploadRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; +import { isAssetChecksumConstraint } from 'src/utils/database'; import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { fromChecksum } from 'src/utils/request'; -interface UploadRequest { - auth: AuthDto | null; - fieldName: UploadFieldName; - file: UploadFile; -} - export interface AssetMediaRedirectResponse { targetSize: AssetMediaSize | 'original'; } @@ -98,15 +92,15 @@ export class AssetMediaService extends BaseService { throw new BadRequestException(`Unsupported file type ${filename}`); } - getUploadFilename({ auth, fieldName, file }: UploadRequest): string { + getUploadFilename({ auth, fieldName, file, body }: UploadRequest): string { requireUploadAccess(auth); - const originalExtension = extname(file.originalName); + const extension = extname(body.filename || file.originalName); const lookup = { - [UploadFieldName.ASSET_DATA]: originalExtension, + [UploadFieldName.ASSET_DATA]: extension, [UploadFieldName.SIDECAR_DATA]: '.xmp', - [UploadFieldName.PROFILE_DATA]: originalExtension, + [UploadFieldName.PROFILE_DATA]: extension, }; return sanitize(`${file.uuid}${lookup[fieldName]}`); @@ -126,8 +120,8 @@ export class AssetMediaService extends BaseService { } async onUploadError(request: AuthRequest, file: Express.Multer.File) { - const uploadFilename = this.getUploadFilename(asRequest(request, file)); - const uploadFolder = this.getUploadFolder(asRequest(request, file)); + const uploadFilename = this.getUploadFilename(asUploadRequest(request, file)); + const uploadFolder = this.getUploadFolder(asUploadRequest(request, file)); const uploadPath = `${uploadFolder}/${uploadFilename}`; await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [uploadPath] } }); @@ -327,7 +321,7 @@ export class AssetMediaService extends BaseService { }); // handle duplicates with a success response - if (error.constraint_name === ASSET_CHECKSUM_CONSTRAINT) { + if (isAssetChecksumConstraint(error)) { const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum); if (!duplicateId) { this.logger.error(`Error locating duplicate for checksum constraint`); @@ -433,6 +427,10 @@ export class AssetMediaService extends BaseService { originalFileName: dto.filename || file.originalName, }); + if (dto.metadata) { + await this.assetRepository.upsertMetadata(asset.id, dto.metadata); + } + if (sidecarFile) { await this.assetRepository.upsertFile({ assetId: asset.id, diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index d05f4f0945..4a7af35d67 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -420,7 +420,7 @@ describe(AssetService.name, () => { ids: ['asset-1'], latitude: 0, longitude: 0, - visibility: undefined, + visibility: AssetVisibility.Archive, isFavorite: false, duplicateId: undefined, rating: undefined, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 7c43eba61b..a22ea364e4 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -9,12 +9,14 @@ import { AssetBulkUpdateDto, AssetJobName, AssetJobsDto, + AssetMetadataResponseDto, + AssetMetadataUpsertDto, AssetStatsDto, UpdateAssetDto, mapStats, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; +import { AssetMetadataKey, 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'; @@ -93,7 +95,7 @@ export class AssetService extends BaseService { } } - await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); + await this.updateExif({ id, description, dateTimeOriginal, latitude, longitude, rating }); const asset = await this.assetRepository.update({ id, ...rest }); @@ -113,59 +115,68 @@ export class AssetService extends BaseService { } async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { - const { ids, description, dateTimeOriginal, dateTimeRelative, timeZone, latitude, longitude, ...options } = dto; + const { + ids, + isFavorite, + visibility, + dateTimeOriginal, + latitude, + longitude, + rating, + description, + duplicateId, + dateTimeRelative, + timeZone, + } = dto; await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids }); - const staticValuesChanged = - description !== undefined || dateTimeOriginal !== undefined || latitude !== undefined || longitude !== undefined; + const assetDto = { isFavorite, visibility, duplicateId }; + const exifDto = { latitude, longitude, rating, description, dateTimeOriginal }; - if (staticValuesChanged) { - await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude }); + const isExifChanged = Object.values(exifDto).some((v) => v !== undefined); + if (isExifChanged) { + await this.assetRepository.updateAllExif(ids, exifDto); } const assets = (dateTimeRelative !== undefined && dateTimeRelative !== 0) || timeZone !== undefined ? await this.assetRepository.updateDateTimeOriginal(ids, dateTimeRelative, timeZone) - : null; + : undefined; - const dateTimesWithTimezone = - assets?.map((asset) => { - const isoString = asset.dateTimeOriginal?.toISOString(); - let dateTime = isoString ? DateTime.fromISO(isoString) : null; + const dateTimesWithTimezone = assets + ? assets.map((asset) => { + const isoString = asset.dateTimeOriginal?.toISOString(); + let dateTime = isoString ? DateTime.fromISO(isoString) : null; - if (dateTime && asset.timeZone) { - dateTime = dateTime.setZone(asset.timeZone); - } + if (dateTime && asset.timeZone) { + dateTime = dateTime.setZone(asset.timeZone); + } - return { - assetId: asset.assetId, - dateTimeOriginal: dateTime?.toISO() ?? null, - }; - }) ?? null; + return { + assetId: asset.assetId, + dateTimeOriginal: dateTime?.toISO() ?? null, + }; + }) + : ids.map((id) => ({ assetId: id, dateTimeOriginal })); - if (staticValuesChanged || dateTimesWithTimezone) { - const entries: JobItem[] = (dateTimesWithTimezone ?? ids).map((entry: any) => ({ - name: JobName.SidecarWrite, - data: { - id: entry.assetId ?? entry, - description, - dateTimeOriginal: entry.dateTimeOriginal ?? dateTimeOriginal, - latitude, - longitude, - }, - })); - await this.jobRepository.queueAll(entries); + if (dateTimesWithTimezone.length > 0) { + await this.jobRepository.queueAll( + dateTimesWithTimezone.map(({ assetId: id, dateTimeOriginal }) => ({ + name: JobName.SidecarWrite, + data: { + ...exifDto, + id, + dateTimeOriginal: dateTimeOriginal ?? undefined, + }, + })), + ); } - if ( - options.visibility !== undefined || - options.isFavorite !== undefined || - options.duplicateId !== undefined || - options.rating !== undefined - ) { - await this.assetRepository.updateAll(ids, options); + const isAssetChanged = Object.values(assetDto).some((v) => v !== undefined); + if (isAssetChanged) { + await this.assetRepository.updateAll(ids, assetDto); - if (options.visibility === AssetVisibility.Locked) { + if (visibility === AssetVisibility.Locked) { await this.albumRepository.removeAssetsFromAll(ids); } } @@ -273,6 +284,31 @@ export class AssetService extends BaseService { }); } + async getMetadata(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); + return this.assetRepository.getMetadata(id); + } + + async upsertMetadata(auth: AuthDto, id: string, dto: AssetMetadataUpsertDto): Promise { + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); + return this.assetRepository.upsertMetadata(id, dto.items); + } + + async getMetadataByKey(auth: AuthDto, id: string, key: AssetMetadataKey): Promise { + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); + + const item = await this.assetRepository.getMetadataByKey(id, key); + if (!item) { + throw new BadRequestException(`Metadata with key "${key}" not found for asset with id "${id}"`); + } + return item; + } + + async deleteMetadataByKey(auth: AuthDto, id: string, key: AssetMetadataKey): Promise { + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); + return this.assetRepository.deleteMetadataByKey(id, key); + } + async run(auth: AuthDto, dto: AssetJobsDto) { await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); @@ -313,7 +349,7 @@ export class AssetService extends BaseService { return asset; } - private async updateMetadata(dto: ISidecarWriteJob) { + private async updateExif(dto: ISidecarWriteJob) { const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); if (Object.keys(writes).length > 0) { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 69d872e8c9..535df779cd 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -344,7 +344,7 @@ export class AuthService extends BaseService { await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldPath] } }); } } catch (error: Error | any) { - this.logger.warn(`Unable to sync oauth profile picture: ${error}`, error?.stack); + this.logger.warn(`Unable to sync oauth profile picture: ${error}\n${error?.stack}`); } } diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index c1cb94b64c..3d99b6e522 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -118,7 +118,7 @@ export class BackupService extends BaseService { { env: { PATH: process.env.PATH, - PGPASSWORD: isUrlConnection ? undefined : config.password, + PGPASSWORD: isUrlConnection ? new URL(config.url).password : config.password, }, }, ); @@ -132,12 +132,12 @@ export class BackupService extends BaseService { gzip.stdout.pipe(fileStream); pgdump.on('error', (err) => { - this.logger.error('Backup failed with error', err); + this.logger.error(`Backup failed with error: ${err}`); reject(err); }); gzip.on('error', (err) => { - this.logger.error('Gzip failed with error', err); + this.logger.error(`Gzip failed with error: ${err}`); reject(err); }); @@ -175,10 +175,10 @@ export class BackupService extends BaseService { }); await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', '')); } catch (error) { - this.logger.error('Database Backup Failure', error); + this.logger.error(`Database Backup Failure: ${error}`); await this.storageRepository .unlink(backupFilePath) - .catch((error) => this.logger.error('Failed to delete failed backup file', error)); + .catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`)); throw error; } diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index 758198a197..2ff0e0ca27 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -22,7 +22,7 @@ const messages = { The ${name} extension version is ${version}, which means it is a nightly release. Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version. - See https://immich.app/docs/guides/database-queries for how to query the database.`, + See https://docs.immich.app/guides/database-queries for how to query the database.`, outOfRange: ({ name, version, range }: OutOfRangeArgs) => `The ${name} extension version is ${version}, but Immich only supports ${range}. Please change ${name} to a compatible version in the Postgres instance.`, @@ -32,20 +32,20 @@ const messages = { 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} CASCADE' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database.`, + See https://docs.immich.app/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. This may be because Immich does not have the necessary permissions to update the extension. Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database.`, + See https://docs.immich.app/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.`, + See https://docs.immich.app/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.`, @@ -55,7 +55,7 @@ const messages = { If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`, deprecatedExtension: (name: string) => `DEPRECATION WARNING: The ${name} extension is deprecated and support for it will be removed very soon. - See https://immich.app/docs/install/upgrading#migrating-to-vectorchord in order to switch to the VectorChord extension instead.`, + See https://docs.immich.app/install/upgrading#migrating-to-vectorchord in order to switch to the VectorChord extension instead.`, }; @Injectable() diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index a758f39244..6b85cdff4d 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -42,6 +42,7 @@ describe(JobService.name, () => { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.AuditTableCleanup }, { name: JobName.AuditLogCleanup }, { name: JobName.MemoryGenerate }, { name: JobName.UserSyncUsage }, diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index a5c48ab43a..dc48c03bd1 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -281,6 +281,7 @@ export class JobService extends BaseService { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.AuditTableCleanup }, { name: JobName.AuditLogCleanup }, ); } diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index f4a1992d91..5f78fa3629 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -245,7 +245,7 @@ export class LibraryService extends BaseService { job.paths.map((path) => this.processEntity(path, library.ownerId, job.libraryId) .then((asset) => assetImports.push(asset)) - .catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}`, error)), + .catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)), ), ); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 7bf9deab4b..1d39169f3e 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -40,7 +40,7 @@ export class MemoryService extends BaseService { try { await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target))); } catch (error) { - this.logger.error(`Failed to create memories for ${target.toISO()}`, error); + this.logger.error(`Failed to create memories for ${target.toISO()}: ${error}`); } // update system metadata even when there is an error to minimize the chance of duplicates await this.systemMetadataRepository.set(SystemMetadataKey.MemoriesState, { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index f483541e72..94704a4707 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -23,16 +23,24 @@ import { tagStub } from 'test/fixtures/tag.stub'; import { factory } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; +function removeNonSidecarFiles(asset: any) { + return { + ...asset, + files: asset.files.filter((file: any) => file.type === AssetFileType.Sidecar), + }; +} + const forSidecarJob = ( asset: { id?: string; originalPath?: string; - files: { id: string; type: AssetFileType; path: string }[]; + files?: { id: string; type: AssetFileType; path: string }[]; } = {}, ) => { return { id: factory.uuid(), originalPath: '/path/to/IMG_123.jpg', + files: [], ...asset, }; }; @@ -1501,7 +1509,7 @@ describe(MetadataService.name, () => { }); describe('handleSidecarCheck', () => { - it("should do nothing if asset isn't found", async () => { + it('should do nothing if asset could not be found', async () => { mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(void 0); await expect(sut.handleSidecarCheck({ id: assetStub.image.id })).resolves.toBeUndefined(); @@ -1510,18 +1518,25 @@ describe(MetadataService.name, () => { }); it('should detect a new sidecar at .jpg.xmp', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg' }); + const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', files: [] }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(true); await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: `/path/to/IMG_123.jpg.xmp` }); + expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ + assetId: asset.id, + type: AssetFileType.Sidecar, + path: '/path/to/IMG_123.jpg.xmp', + }); }); it('should detect a new sidecar at .xmp', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg' }); + const asset = forSidecarJob({ + originalPath: '/path/to/IMG_123.jpg', + files: [], + }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(false); @@ -1529,29 +1544,39 @@ describe(MetadataService.name, () => { await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: '/path/to/IMG_123.xmp' }); + expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ + assetId: asset.id, + type: AssetFileType.Sidecar, + path: '/path/to/IMG_123.xmp', + }); }); - it('should unset sidecar path if file does not exist anymore', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', sidecarPath: '/path/to/IMG_123.jpg.xmp' }); + it('should unset sidecar path if file no longer exist', async () => { + const asset = forSidecarJob({ + originalPath: '/path/to/IMG_123.jpg', + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValue(false); await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: null }); + expect(mocks.asset.deleteFile).toHaveBeenCalledWith({ assetId: asset.id, type: AssetFileType.Sidecar }); }); it('should do nothing if the sidecar file still exists', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', sidecarPath: '/path/to/IMG_123.jpg' }); + const asset = forSidecarJob({ + originalPath: '/path/to/IMG_123.jpg', + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(true); await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Skipped); - expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFile).not.toHaveBeenCalled(); }); }); @@ -1669,5 +1694,16 @@ describe(MetadataService.name, () => { expect(result?.tag).toBe('GPSDateTime'); expect(result?.dateTime?.toDate()?.toISOString()).toBe('2023-10-10T10:00:00.000Z'); }); + + it('should prefer CreationDate over CreateDate', () => { + const tags = { + CreationDate: '2025:05:24 18:26:20+02:00', + CreateDate: '2025:08:27 08:45:40', + }; + + const result = firstDateTime(tags); + expect(result?.tag).toBe('CreationDate'); + expect(result?.dateTime?.toDate()?.toISOString()).toBe('2025-05-24T16:26:20.000Z'); + }); }); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index b16de38ae4..790bd81c92 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -30,6 +30,7 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; +import { isAssetChecksumConstraint } from 'src/utils/database'; import { isFaceImportEnabled } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; @@ -39,9 +40,9 @@ const EXIF_DATE_TAGS: Array = [ 'SubSecCreateDate', 'SubSecMediaCreateDate', 'DateTimeOriginal', + 'CreationDate', 'CreateDate', 'MediaCreateDate', - 'CreationDate', 'DateTimeCreated', 'GPSDateTime', 'DateTimeUTC', @@ -372,11 +373,9 @@ export class MetadataService extends BaseService { return JobStatus.Skipped; } - if (sidecarPath === null) { - await this.assetRepository.deleteFile({ assetId: asset.id, type: AssetFileType.Sidecar }); - } else { - await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath }); - } + await (sidecarPath === null + ? this.assetRepository.deleteFile({ assetId: asset.id, type: AssetFileType.Sidecar }) + : this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath })); return JobStatus.Success; } @@ -431,6 +430,12 @@ export class MetadataService extends BaseService { private getSidecarCandidates({ files, originalPath }: { files: AssetFile[] | null; originalPath: string }) { const candidates: string[] = []; + const existingSidecar = files?.find((file) => file.type === AssetFileType.Sidecar); + + if (existingSidecar) { + candidates.push(existingSidecar.path); + } + const assetPath = parse(originalPath); candidates.push( @@ -598,47 +603,62 @@ export class MetadataService extends BaseService { }); } const checksum = this.cryptoRepository.hashSha1(video); + const checksumQuery = { ownerId: asset.ownerId, libraryId: asset.libraryId ?? undefined, checksum }; - let motionAsset = await this.assetRepository.getByChecksum({ - ownerId: asset.ownerId, - libraryId: asset.libraryId ?? undefined, - checksum, - }); - if (motionAsset) { + let motionAsset = await this.assetRepository.getByChecksum(checksumQuery); + let isNewMotionAsset = false; + + if (!motionAsset) { + try { + const motionAssetId = this.cryptoRepository.randomUUID(); + motionAsset = await this.assetRepository.create({ + id: motionAssetId, + libraryId: asset.libraryId, + type: AssetType.Video, + fileCreatedAt: dates.dateTimeOriginal, + fileModifiedAt: stats.mtime, + localDateTime: dates.localDateTime, + checksum, + ownerId: asset.ownerId, + originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), + originalFileName: `${parse(asset.originalFileName).name}.mp4`, + visibility: AssetVisibility.Hidden, + deviceAssetId: 'NONE', + deviceId: 'NONE', + }); + + isNewMotionAsset = true; + + if (!asset.isExternal) { + await this.userRepository.updateUsage(asset.ownerId, video.byteLength); + } + } catch (error) { + if (!isAssetChecksumConstraint(error)) { + throw error; + } + + motionAsset = await this.assetRepository.getByChecksum(checksumQuery); + if (!motionAsset) { + this.logger.warn(`Unable to find existing motion video asset for ${asset.id}: ${asset.originalPath}`); + return; + } + } + } + + if (!isNewMotionAsset) { this.logger.debugFn(() => { const base64Checksum = checksum.toString('base64'); return `Motion asset with checksum ${base64Checksum} already exists for asset ${asset.id}: ${asset.originalPath}`; }); + } - // Hide the motion photo video asset if it's not already hidden to prepare for linking - if (motionAsset.visibility === AssetVisibility.Timeline) { - await this.assetRepository.update({ - id: motionAsset.id, - visibility: AssetVisibility.Hidden, - }); - this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`); - } - } else { - const motionAssetId = this.cryptoRepository.randomUUID(); - motionAsset = await this.assetRepository.create({ - id: motionAssetId, - libraryId: asset.libraryId, - type: AssetType.Video, - fileCreatedAt: dates.dateTimeOriginal, - fileModifiedAt: stats.mtime, - localDateTime: dates.localDateTime, - checksum, - ownerId: asset.ownerId, - originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), - originalFileName: `${parse(asset.originalFileName).name}.mp4`, + // Hide the motion photo video asset if it's not already hidden to prepare for linking + if (motionAsset.visibility === AssetVisibility.Timeline) { + await this.assetRepository.update({ + id: motionAsset.id, visibility: AssetVisibility.Hidden, - deviceAssetId: 'NONE', - deviceId: 'NONE', }); - - if (!asset.isExternal) { - await this.userRepository.updateUsage(asset.ownerId, video.byteLength); - } + this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`); } if (asset.livePhotoVideoId !== motionAsset.id) { @@ -830,7 +850,11 @@ export class MetadataService extends BaseService { } } - private getDates(asset: { id: string; originalPath: string }, exifTags: ImmichTags, stats: Stats) { + private getDates( + asset: { id: string; originalPath: string; fileCreatedAt: Date }, + exifTags: ImmichTags, + stats: Stats, + ) { const result = firstDateTime(exifTags); const tag = result?.tag; const dateTime = result?.dateTime; @@ -859,7 +883,12 @@ export class MetadataService extends BaseService { if (!localDateTime || !dateTimeOriginal) { // FileCreateDate is not available on linux, likely because exiftool hasn't integrated the statx syscall yet // birthtime is not available in Docker on macOS, so it appears as 0 - const earliestDate = stats.birthtimeMs ? new Date(Math.min(stats.mtimeMs, stats.birthtimeMs)) : stats.mtime; + const earliestDate = new Date( + Math.min( + asset.fileCreatedAt.getTime(), + stats.birthtimeMs ? Math.min(stats.mtimeMs, stats.birthtimeMs) : stats.mtime.getTime(), + ), + ); this.logger.debug( `No exif date time found, falling back on ${earliestDate.toISOString()}, earliest of file creation and modification for asset ${asset.id}: ${asset.originalPath}`, ); diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index eef1c4f8b2..11c385b1e2 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -147,7 +147,7 @@ describe(NotificationService.name, () => { await sut.onUserSignup({ id: '', notify: true }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.NotifyUserSignup, - data: { id: '', tempPassword: undefined }, + data: { id: '', password: undefined }, }); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 1a257309b2..91a043d405 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -191,9 +191,9 @@ export class NotificationService extends BaseService { } @OnEvent({ name: 'UserSignup' }) - async onUserSignup({ notify, id, tempPassword }: ArgOf<'UserSignup'>) { + async onUserSignup({ notify, id, password: password }: ArgOf<'UserSignup'>) { if (notify) { - await this.jobRepository.queue({ name: JobName.NotifyUserSignup, data: { id, tempPassword } }); + await this.jobRepository.queue({ name: JobName.NotifyUserSignup, data: { id, password } }); } } @@ -251,70 +251,8 @@ export class NotificationService extends BaseService { return { messageId }; } - async getTemplate(name: EmailTemplate, customTemplate: string) { - const { server, templates } = await this.getConfig({ withCache: false }); - - let templateResponse = ''; - - switch (name) { - case EmailTemplate.WELCOME: { - const { html: _welcomeHtml } = await this.emailRepository.renderEmail({ - template: EmailTemplate.WELCOME, - data: { - baseUrl: getExternalDomain(server), - displayName: 'John Doe', - username: 'john@doe.com', - password: 'thisIsAPassword123', - }, - customTemplate: customTemplate || templates.email.welcomeTemplate, - }); - - templateResponse = _welcomeHtml; - break; - } - case EmailTemplate.ALBUM_UPDATE: { - const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({ - template: EmailTemplate.ALBUM_UPDATE, - data: { - baseUrl: getExternalDomain(server), - albumId: '1', - albumName: 'Favorite Photos', - recipientName: 'Jane Doe', - cid: undefined, - }, - customTemplate: customTemplate || templates.email.albumInviteTemplate, - }); - templateResponse = _updateAlbumHtml; - break; - } - - case EmailTemplate.ALBUM_INVITE: { - const { html } = await this.emailRepository.renderEmail({ - template: EmailTemplate.ALBUM_INVITE, - data: { - baseUrl: getExternalDomain(server), - albumId: '1', - albumName: "John Doe's Favorites", - senderName: 'John Doe', - recipientName: 'Jane Doe', - cid: undefined, - }, - customTemplate: customTemplate || templates.email.albumInviteTemplate, - }); - templateResponse = html; - break; - } - default: { - templateResponse = ''; - break; - } - } - - return { name, html: templateResponse }; - } - @OnJob({ name: JobName.NotifyUserSignup, queue: QueueName.Notification }) - async handleUserSignup({ id, tempPassword }: JobOf) { + async handleUserSignup({ id, password }: JobOf) { const user = await this.userRepository.get(id, { withDeleted: false }); if (!user) { return JobStatus.Skipped; @@ -327,7 +265,7 @@ export class NotificationService extends BaseService { baseUrl: getExternalDomain(server), displayName: user.name, username: user.email, - password: tempPassword, + password, }, customTemplate: templates.email.welcomeTemplate, }); diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index c6d5762c2c..db057a453a 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -53,7 +53,7 @@ describe(PartnerService.name, () => { mocks.partner.get.mockResolvedValue(void 0); mocks.partner.create.mockResolvedValue(partner); - await expect(sut.create(auth, user2.id)).resolves.toBeDefined(); + await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined(); expect(mocks.partner.create).toHaveBeenCalledWith({ sharedById: partner.sharedById, @@ -69,7 +69,7 @@ describe(PartnerService.name, () => { mocks.partner.get.mockResolvedValue(partner); - await expect(sut.create(auth, user2.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException); expect(mocks.partner.create).not.toHaveBeenCalled(); }); diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index 755b688397..628efa9d49 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Partner } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; -import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; import { Permission } from 'src/enum'; import { PartnerDirection, PartnerIds } from 'src/repositories/partner.repository'; @@ -9,7 +9,7 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class PartnerService extends BaseService { - async create(auth: AuthDto, sharedWithId: string): Promise { + async create(auth: AuthDto, { sharedWithId }: PartnerCreateDto): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const exists = await this.partnerRepository.get(partnerId); if (exists) { @@ -39,7 +39,7 @@ export class PartnerService extends BaseService { .map((partner) => this.mapPartner(partner, direction)); } - async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { + async update(auth: AuthDto, sharedById: string, dto: PartnerUpdateDto): Promise { await this.requireAccess({ auth, permission: Permission.PartnerUpdate, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 13c3128317..41c44ea476 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -729,7 +729,6 @@ describe(PersonService.name, () => { mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); await sut.handleDetectFaces({ id: assetStub.image.id }); expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith( - ['http://immich-machine-learning:3003'], '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 344b69efde..6fa9b3fdd2 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -316,7 +316,6 @@ export class PersonService extends BaseService { } const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( - machineLearning.urls, previewFile.path, machineLearning.facialRecognition, ); diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index d87ccbde1d..b6e09add19 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -211,7 +211,6 @@ describe(SearchService.name, () => { await sut.searchSmart(authStub.user1, { query: 'test' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( - [expect.any(String)], 'test', expect.objectContaining({ modelName: expect.any(String) }), ); @@ -225,7 +224,6 @@ describe(SearchService.name, () => { await sut.searchSmart(authStub.user1, { query: 'test', page: 2, size: 50 }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( - [expect.any(String)], 'test', expect.objectContaining({ modelName: expect.any(String) }), ); @@ -243,7 +241,6 @@ describe(SearchService.name, () => { await sut.searchSmart(authStub.user1, { query: 'test' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( - [expect.any(String)], 'test', expect.objectContaining({ modelName: 'ViT-B-16-SigLIP__webli' }), ); @@ -253,7 +250,6 @@ describe(SearchService.name, () => { await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( - [expect.any(String)], 'test', expect.objectContaining({ language: 'de' }), ); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index b9391fed90..fea1670e27 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -18,7 +18,7 @@ import { SmartSearchDto, StatisticsSearchDto, } from 'src/dtos/search.dto'; -import { AssetOrder, AssetVisibility } from 'src/enum'; +import { AssetOrder, AssetVisibility, Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; @@ -113,14 +113,27 @@ export class SearchService extends BaseService { } const userIds = this.getUserIdsToSearch(auth); - const key = machineLearning.clip.modelName + dto.query + dto.language; - let embedding = this.embeddingCache.get(key); - if (!embedding) { - embedding = await this.machineLearningRepository.encodeText(machineLearning.urls, dto.query, { - modelName: machineLearning.clip.modelName, - language: dto.language, - }); - this.embeddingCache.set(key, embedding); + let embedding; + if (dto.query) { + const key = machineLearning.clip.modelName + dto.query + dto.language; + embedding = this.embeddingCache.get(key); + if (!embedding) { + embedding = await this.machineLearningRepository.encodeText(dto.query, { + modelName: machineLearning.clip.modelName, + language: dto.language, + }); + this.embeddingCache.set(key, embedding); + } + } else if (dto.queryAssetId) { + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.queryAssetId] }); + const getEmbeddingResponse = await this.searchRepository.getEmbedding(dto.queryAssetId); + const assetEmbedding = getEmbeddingResponse?.embedding; + if (!assetEmbedding) { + throw new BadRequestException(`Asset ${dto.queryAssetId} has no embedding`); + } + embedding = assetEmbedding; + } else { + throw new BadRequestException('Either `query` or `queryAssetId` must be set'); } const page = dto.page ?? 1; const size = dto.size || 100; diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index edd9f4663a..b3af5cd15f 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -205,7 +205,6 @@ describe(SmartInfoService.name, () => { expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( - ['http://immich-machine-learning:3003'], '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); @@ -242,7 +241,6 @@ describe(SmartInfoService.name, () => { expect(mocks.database.wait).toHaveBeenCalledWith(512); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( - ['http://immich-machine-learning:3003'], '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 3b8e2d1fc3..eff16fea45 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -108,11 +108,7 @@ export class SmartInfoService extends BaseService { return JobStatus.Skipped; } - const embedding = await this.machineLearningRepository.encodeImage( - machineLearning.urls, - asset.files[0].path, - machineLearning.clip, - ); + const embedding = await this.machineLearningRepository.encodeImage(asset.files[0].path, machineLearning.clip); if (this.databaseRepository.isBusy(DatabaseLock.CLIPDimSize)) { this.logger.verbose(`Waiting for CLIP dimension size to be updated`); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 6086d62809..1d38bf7011 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -338,7 +338,7 @@ export class StorageTemplateService extends BaseService { return destination; } catch (error: any) { - this.logger.error(`Unable to get template path for ${filename}`, error); + this.logger.error(`Unable to get template path for ${filename}: ${error}`); return asset.originalPath; } } diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index b983c34f62..50dffd5465 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -15,7 +15,7 @@ import { BaseService } from 'src/services/base.service'; import { JobOf, SystemFlags } from 'src/types'; import { ImmichStartupError } from 'src/utils/misc'; -const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`; +const docsMessage = `Please see https://docs.immich.app/administration/system-integrity#folder-checks for more information.`; @Injectable() export class StorageService extends BaseService { diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 6b8512eacb..f354a71791 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,8 +1,9 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; -import { DateTime } from 'luxon'; +import { DateTime, Duration } from 'luxon'; import { Writable } from 'node:stream'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; +import { OnJob } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { @@ -15,7 +16,16 @@ import { SyncItem, SyncStreamDto, } from 'src/dtos/sync.dto'; -import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum'; +import { + AssetVisibility, + DatabaseAction, + EntityType, + JobName, + Permission, + QueueName, + SyncEntityType, + SyncRequestType, +} from 'src/enum'; import { SyncQueryOptions } from 'src/repositories/sync.repository'; import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table'; import { BaseService } from 'src/services/base.service'; @@ -32,6 +42,8 @@ type AssetLike = Omit & { }; const COMPLETE_ID = 'complete'; +const MAX_DAYS = 30; +const MAX_DURATION = Duration.fromObject({ days: MAX_DAYS }); const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({ ...data, @@ -74,6 +86,7 @@ export const SYNC_TYPES_ORDER = [ SyncRequestType.PeopleV1, SyncRequestType.AssetFacesV1, SyncRequestType.UserMetadataV1, + SyncRequestType.AssetMetadataV1, ]; const throwSessionRequired = () => { @@ -136,19 +149,24 @@ export class SyncService extends BaseService { } const isPendingSyncReset = await this.sessionRepository.isPendingSyncReset(session.id); - if (isPendingSyncReset) { send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} }); response.end(); return; } + const checkpoints = await this.syncCheckpointRepository.getAll(session.id); + const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); + + if (this.needsFullSync(checkpointMap)) { + send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} }); + response.end(); + return; + } + const { nowId } = await this.syncCheckpointRepository.getNow(); const options: SyncQueryOptions = { nowId, userId: auth.user.id }; - const checkpoints = await this.syncCheckpointRepository.getAll(session.id); - const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); - const handlers: Record Promise> = { [SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap), [SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap), @@ -156,6 +174,7 @@ export class SyncService extends BaseService { [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap), [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap), [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id), + [SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth), [SyncRequestType.PartnerAssetExifsV1]: () => this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id), [SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap), @@ -178,9 +197,41 @@ export class SyncService extends BaseService { await handler(); } + send(response, { type: SyncEntityType.SyncCompleteV1, ids: [nowId], data: {} }); + response.end(); } + @OnJob({ name: JobName.AuditTableCleanup, queue: QueueName.BackgroundTask }) + async onAuditTableCleanup() { + const pruneThreshold = MAX_DAYS + 1; + + await this.syncRepository.album.cleanupAuditTable(pruneThreshold); + await this.syncRepository.albumUser.cleanupAuditTable(pruneThreshold); + await this.syncRepository.albumToAsset.cleanupAuditTable(pruneThreshold); + await this.syncRepository.asset.cleanupAuditTable(pruneThreshold); + await this.syncRepository.assetFace.cleanupAuditTable(pruneThreshold); + await this.syncRepository.assetMetadata.cleanupAuditTable(pruneThreshold); + await this.syncRepository.memory.cleanupAuditTable(pruneThreshold); + await this.syncRepository.memoryToAsset.cleanupAuditTable(pruneThreshold); + await this.syncRepository.partner.cleanupAuditTable(pruneThreshold); + await this.syncRepository.person.cleanupAuditTable(pruneThreshold); + await this.syncRepository.stack.cleanupAuditTable(pruneThreshold); + await this.syncRepository.user.cleanupAuditTable(pruneThreshold); + await this.syncRepository.userMetadata.cleanupAuditTable(pruneThreshold); + } + + private needsFullSync(checkpointMap: CheckpointMap) { + const completeAck = checkpointMap[SyncEntityType.SyncCompleteV1]; + if (!completeAck) { + return false; + } + + const milliseconds = Number.parseInt(completeAck.updateId.replaceAll('-', '').slice(0, 12), 16); + + return DateTime.fromMillis(milliseconds) < DateTime.now().minus(MAX_DURATION); + } + private async syncAuthUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { const upsertType = SyncEntityType.AuthUserV1; const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] }); @@ -717,13 +768,13 @@ export class SyncService extends BaseService { private async syncPeopleV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { const deleteType = SyncEntityType.PersonDeleteV1; - const deletes = this.syncRepository.people.getDeletes({ ...options, ack: checkpointMap[deleteType] }); + const deletes = this.syncRepository.person.getDeletes({ ...options, ack: checkpointMap[deleteType] }); for await (const { id, ...data } of deletes) { send(response, { type: deleteType, ids: [id], data }); } const upsertType = SyncEntityType.PersonV1; - const upserts = this.syncRepository.people.getUpserts({ ...options, ack: checkpointMap[upsertType] }); + const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { send(response, { type: upsertType, ids: [updateId], data }); } @@ -759,6 +810,33 @@ export class SyncService extends BaseService { } } + private async syncAssetMetadataV1( + options: SyncQueryOptions, + response: Writable, + checkpointMap: CheckpointMap, + auth: AuthDto, + ) { + const deleteType = SyncEntityType.AssetMetadataDeleteV1; + const deletes = this.syncRepository.assetMetadata.getDeletes( + { ...options, ack: checkpointMap[deleteType] }, + auth.user.id, + ); + + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + + const upsertType = SyncEntityType.AssetMetadataV1; + const upserts = this.syncRepository.assetMetadata.getUpserts( + { ...options, ack: checkpointMap[upsertType] }, + auth.user.id, + ); + + for await (const { updateId, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data }); + } + } + private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) { const { type, sessionId, createId } = item; await this.syncCheckpointRepository.upsertAll([ diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 20127bab15..5a9c7f4df3 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -52,7 +52,7 @@ const updatedConfig = Object.freeze({ threads: 0, preset: 'ultrafast', targetAudioCodec: AudioCodec.Aac, - acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le], + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus], targetResolution: '720', targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], @@ -82,6 +82,11 @@ const updatedConfig = Object.freeze({ machineLearning: { enabled: true, urls: ['http://immich-machine-learning:3003'], + availabilityChecks: { + enabled: true, + interval: 30_000, + timeout: 2000, + }, clip: { enabled: true, modelName: 'ViT-B-32__openai', diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index d046b0317a..ea95b4df24 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -16,6 +16,20 @@ export class SystemConfigService extends BaseService { async onBootstrap() { const config = await this.getConfig({ withCache: false }); await this.eventRepository.emit('ConfigInit', { newConfig: config }); + + if ( + process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT || + process.env.IMMICH_MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME + ) { + this.logger.deprecate( + 'IMMICH_MACHINE_LEARNING_PING_TIMEOUT and MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME have been moved to system config(`machineLearning.availabilityChecks`) and will be removed in a future release.', + ); + } + } + + @OnEvent({ name: 'AppShutdown' }) + onShutdown() { + this.machineLearningRepository.teardown(); } async getSystemConfig(): Promise { @@ -28,12 +42,14 @@ export class SystemConfigService extends BaseService { } @OnEvent({ name: 'ConfigInit', priority: -100 }) - onConfigInit({ newConfig: { logging } }: ArgOf<'ConfigInit'>) { + onConfigInit({ newConfig: { logging, machineLearning } }: ArgOf<'ConfigInit'>) { const { logLevel: envLevel } = this.configRepository.getEnv(); const configLevel = logging.enabled ? logging.level : false; const level = envLevel ?? configLevel; this.logger.setLogLevel(level); this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`); + + this.machineLearningRepository.setup(machineLearning); } @OnEvent({ name: 'ConfigUpdate', server: true }) diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 3ae9d429eb..ce70419ff6 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -38,7 +38,7 @@ export class UserAdminService extends BaseService { await this.eventRepository.emit('UserSignup', { notify: !!notify, id: user.id, - tempPassword: user.shouldChangePassword ? userDto.password : undefined, + password: userDto.password, }); return mapUserAdmin(user); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index c4d7e9974d..b817363eac 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -95,7 +95,7 @@ export class VersionService extends BaseService { this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata)); } } catch (error: Error | any) { - this.logger.warn(`Unable to run version check: ${error}`, error?.stack); + this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`); return JobStatus.Failed; } diff --git a/server/src/types.ts b/server/src/types.ts index eeec71a601..9ba0ef6491 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,6 +1,9 @@ import { SystemConfig } from 'src/config'; import { VECTOR_EXTENSIONS } from 'src/constants'; +import { UploadFieldName } from 'src/dtos/asset-media.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { + AssetMetadataKey, AssetOrder, AssetType, DatabaseSslMode, @@ -246,7 +249,7 @@ export interface IEmailJob { } export interface INotifySignupJob extends IEntityJob { - tempPassword?: string; + password?: string; } export interface INotifyAlbumInviteJob extends IEntityJob { @@ -272,6 +275,9 @@ export interface QueueStatus { } export type JobItem = + // Audit + | { name: JobName.AuditTableCleanup; data?: IBaseJob } + // Backups | { name: JobName.DatabaseBackup; data?: IBaseJob } @@ -406,6 +412,16 @@ export interface UploadFile { size: number; } +export type UploadRequest = { + auth: AuthDto | null; + fieldName: UploadFieldName; + file: UploadFile; + body: { + filename?: string; + [key: string]: unknown; + }; +}; + export interface UploadFiles { assetData: ImmichFile[]; sidecarData: ImmichFile[]; @@ -464,11 +480,6 @@ export interface SystemMetadata extends Record = { - key: T; - value: UserMetadata[T]; -}; - export interface UserPreferences { albums: { defaultAssetOrder: AssetOrder; @@ -513,8 +524,22 @@ export interface UserPreferences { }; } +export type UserMetadataItem = { + key: T; + value: UserMetadata[T]; +}; + export interface UserMetadata extends Record> { [UserMetadataKey.Preferences]: DeepPartial; [UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string }; [UserMetadataKey.Onboarding]: { isOnboarded: boolean }; } + +export type AssetMetadataItem = { + key: T; + value: AssetMetadata[T]; +}; + +export interface AssetMetadata extends Record> { + [AssetMetadataKey.MobileApp]: { iCloudId: string }; +} diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 2d1bb96c53..f3f807c829 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -10,7 +10,7 @@ import { AccessRepository } from 'src/repositories/access.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; -import { IBulkAsset, ImmichFile, UploadFile } from 'src/types'; +import { IBulkAsset, ImmichFile, UploadFile, UploadRequest } from 'src/types'; import { checkAccess } from 'src/utils/access'; export const getAssetFile = (files: AssetFile[], type: AssetFileType | GeneratedImageType) => { @@ -191,9 +191,10 @@ export function mapToUploadFile(file: ImmichFile): UploadFile { }; } -export const asRequest = (request: AuthRequest, file: Express.Multer.File) => { +export const asUploadRequest = (request: AuthRequest, file: Express.Multer.File): UploadRequest => { return { auth: request.user || null, + body: request.body, fieldName: file.fieldname as UploadFieldName, file: mapToUploadFile(file as ImmichFile), }; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 1ef9b8e926..d9fe6b7897 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -14,7 +14,7 @@ import { import { PostgresJSDialect } from 'kysely-postgres-js'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import { parse } from 'pg-connection-string'; -import postgres, { Notice } from 'postgres'; +import postgres, { Notice, PostgresError } from 'postgres'; import { columns, Exif, Person } from 'src/database'; import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; @@ -153,6 +153,10 @@ export function toJson { + return (error as PostgresError)?.constraint_name === 'UQ_assets_owner_checksum'; +}; + export function withDefaultVisibility(qb: SelectQueryBuilder) { return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.Archive), sql.lit(AssetVisibility.Timeline)]); } diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index 2331a45a62..29c7f6f772 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -73,7 +73,7 @@ export const sendFile = async ( // log non-http errors if (error instanceof HttpException === false) { - logger.error(`Unable to send file: ${error.name}`, error.stack); + logger.error(`Unable to send file: ${error}`, error.stack); } res.header('Cache-Control', 'none'); diff --git a/server/src/validation.ts b/server/src/validation.ts index e583f6a44e..6d4bbfbe36 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -211,6 +211,18 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { return applyDecorators(...decorators); }; +type StringOptions = { optional?: boolean; nullable?: boolean; trim?: boolean }; +export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => { + const { optional, nullable, trim, ...apiPropertyOptions } = options || {}; + const decorators = [ApiProperty(apiPropertyOptions), IsString(), optional ? Optional({ nullable }) : IsNotEmpty()]; + + if (trim) { + decorators.push(Transform(({ value }: { value: string }) => value?.trim())); + } + + return applyDecorators(...decorators); +}; + type BooleanOptions = { optional?: boolean; nullable?: boolean }; export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) => { const { optional, nullable, ...apiPropertyOptions } = options || {}; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index d4bbdeeb18..937836a9bf 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -47,7 +47,7 @@ export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif primaryAssetId: assets[0].id, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), - updateId: 'uuid-v7', + updateId: expect.any(String), }; }; diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index 7a2cacf126..ca66af7b94 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,5 +1,6 @@ import { Tag } from 'src/database'; import { TagResponseDto } from 'src/dtos/tag.dto'; +import { newUuidV7 } from 'test/small.factory'; const parent = Object.freeze({ id: 'tag-parent', @@ -37,7 +38,10 @@ const color = { parentId: null, }; -const upsert = { userId: 'tag-user', updateId: 'uuid-v7' }; +const upsert = { + userId: 'tag-user', + updateId: newUuidV7(), +}; export const tagStub = { tag, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 87c8406f55..a169d96322 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -258,6 +258,12 @@ export class SyncTestContext extends MediumTestContext { return stream.getResponse(); } + async assertSyncIsComplete(auth: AuthDto, types: SyncRequestType[]) { + await expect(this.syncStream(auth, types)).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + } + async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) { const acks: Record = {}; const syncAcks: string[] = []; diff --git a/server/test/medium/specs/services/sync.service.spec.ts b/server/test/medium/specs/services/sync.service.spec.ts new file mode 100644 index 0000000000..b5443d7e62 --- /dev/null +++ b/server/test/medium/specs/services/sync.service.spec.ts @@ -0,0 +1,226 @@ +import { Kysely } from 'kysely'; +import { DateTime } from 'luxon'; +import { AssetMetadataKey, UserMetadataKey } from 'src/enum'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { SyncRepository } from 'src/repositories/sync.repository'; +import { DB } from 'src/schema'; +import { SyncService } from 'src/services/sync.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; +import { v4 } from 'uuid'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(SyncService, { + database: db || defaultDatabase, + real: [DatabaseRepository, SyncRepository], + mock: [LoggingRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +const deletedLongAgo = DateTime.now().minus({ days: 35 }).toISO(); + +const assertTableCount = async (db: Kysely, t: T, count: number) => { + const { table } = db.dynamic; + const results = await db.selectFrom(table(t).as(t)).selectAll().execute(); + expect(results).toHaveLength(count); +}; + +describe(SyncService.name, () => { + describe('onAuditTableCleanup', () => { + it('should work', async () => { + const { sut } = setup(); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + }); + + it('should cleanup the album_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'album_audit'; + + await ctx.database + .insertInto(tableName) + .values({ albumId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the album_asset_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'album_asset_audit'; + const { user } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + await ctx.database + .insertInto(tableName) + .values({ albumId: album.id, assetId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the album_user_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'album_user_audit'; + await ctx.database + .insertInto(tableName) + .values({ albumId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the asset_audit table', async () => { + const { sut, ctx } = setup(); + + await ctx.database + .insertInto('asset_audit') + .values({ assetId: v4(), ownerId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, 'asset_audit', 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, 'asset_audit', 0); + }); + + it('should cleanup the asset_face_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'asset_face_audit'; + await ctx.database + .insertInto(tableName) + .values({ assetFaceId: v4(), assetId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the asset_metadata_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'asset_metadata_audit'; + await ctx.database + .insertInto(tableName) + .values({ assetId: v4(), key: AssetMetadataKey.MobileApp, deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the memory_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'memory_audit'; + await ctx.database + .insertInto(tableName) + .values({ memoryId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the memory_asset_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'memory_asset_audit'; + const { user } = await ctx.newUser(); + const { memory } = await ctx.newMemory({ ownerId: user.id }); + await ctx.database + .insertInto(tableName) + .values({ memoryId: memory.id, assetId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the partner_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'partner_audit'; + await ctx.database + .insertInto(tableName) + .values({ sharedById: v4(), sharedWithId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the stack_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'stack_audit'; + await ctx.database + .insertInto(tableName) + .values({ stackId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the user_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'user_audit'; + await ctx.database.insertInto(tableName).values({ userId: v4(), deletedAt: deletedLongAgo }).execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the user_metadata_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'user_metadata_audit'; + await ctx.database + .insertInto(tableName) + .values({ userId: v4(), key: UserMetadataKey.Onboarding, deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should skip recent records', async () => { + const { sut, ctx } = setup(); + + const keep = { + id: v4(), + assetId: v4(), + ownerId: v4(), + deletedAt: DateTime.now().minus({ days: 25 }).toISO(), + }; + + const remove = { + id: v4(), + assetId: v4(), + ownerId: v4(), + deletedAt: DateTime.now().minus({ days: 35 }).toISO(), + }; + + await ctx.database.insertInto('asset_audit').values([keep, remove]).execute(); + await assertTableCount(ctx.database, 'asset_audit', 2); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + + const after = await ctx.database.selectFrom('asset_audit').select(['id']).execute(); + expect(after).toHaveLength(1); + expect(after[0].id).toBe(keep.id); + }); + }); +}); diff --git a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts index 9e994604a5..fd563f4db1 100644 --- a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts @@ -74,11 +74,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }, type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(2); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should sync album asset exif for own user', async () => { @@ -88,8 +88,15 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(2); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.SyncAckV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumAssetExifCreateV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); }); it('should not sync album asset exif for unrelated user', async () => { @@ -104,8 +111,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should backfill album assets exif when a user shares an album with you', async () => { @@ -139,8 +149,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(2); // ack initial album asset exif sync await ctx.syncAckAll(auth, response); @@ -174,11 +184,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(newResponse).toHaveLength(5); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should sync old asset exif when a user adds them to an album they share you', async () => { @@ -207,8 +217,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(firstAlbumResponse).toHaveLength(2); await ctx.syncAckAll(auth, firstAlbumResponse); @@ -224,8 +234,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { type: SyncEntityType.AlbumAssetExifBackfillV1, }, backfillSyncAck, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(2); // ack initial album asset sync await ctx.syncAckAll(auth, response); @@ -244,11 +254,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(newResponse).toHaveLength(2); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should sync asset exif updates for an album shared with you', async () => { @@ -262,7 +272,6 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -272,6 +281,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -283,9 +293,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { city: 'New City', }); - const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(updateResponse).toHaveLength(1); - expect(updateResponse).toEqual([ + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -294,6 +302,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifUpdateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); @@ -330,8 +339,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(3); await ctx.syncAckAll(auth, response); @@ -342,8 +351,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { city: 'Delayed Exif', }); - const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(updateResponse).toEqual([ + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -352,7 +360,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifUpdateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(updateResponse).toHaveLength(1); }); }); diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index cbc60a2c5a..4f053937b8 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -58,7 +58,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -83,10 +82,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }, type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should sync album asset for own user', async () => { @@ -95,8 +95,15 @@ describe(SyncRequestType.AlbumAssetsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(2); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.SyncAckV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumAssetCreateV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); }); it('should not sync album asset for unrelated user', async () => { @@ -110,8 +117,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should backfill album assets when a user shares an album with you', async () => { @@ -133,7 +143,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -143,6 +152,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial album asset sync @@ -176,10 +186,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should sync old assets when a user adds them to an album they share you', async () => { @@ -196,7 +207,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(firstAlbumResponse).toHaveLength(2); expect(firstAlbumResponse).toEqual([ updateSyncAck, { @@ -206,6 +216,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, firstAlbumResponse); @@ -213,7 +224,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - // expect(response).toHaveLength(2); expect(response).toEqual([ { ack: expect.any(String), @@ -223,6 +233,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { type: SyncEntityType.AlbumAssetBackfillV1, }, backfillSyncAck, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial album asset sync @@ -242,10 +253,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should sync asset updates for an album shared with you', async () => { @@ -258,7 +270,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -268,6 +279,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -280,7 +292,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }); const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(updateResponse).toHaveLength(1); expect(updateResponse).toEqual([ { ack: expect.any(String), @@ -290,6 +301,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetUpdateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index ee529c5001..b6bd9db010 100644 --- a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts @@ -28,7 +28,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -38,10 +37,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should sync album to asset for owned albums', async () => { @@ -51,7 +51,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -61,10 +60,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should detect and sync the album to asset for shared albums', async () => { @@ -76,7 +76,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -86,10 +85,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should not sync album to asset for an album owned by another user', async () => { @@ -98,7 +98,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should backfill album to assets when a user shares an album with you', async () => { @@ -114,7 +114,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -124,6 +123,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial album to asset sync @@ -148,10 +148,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should detect and sync a deleted album to asset relation', async () => { @@ -162,7 +163,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -172,6 +172,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -179,7 +180,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await wait(2); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -189,10 +189,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should detect and sync a deleted album to asset relation when an asset is deleted', async () => { @@ -203,7 +204,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -213,6 +213,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -220,7 +221,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await wait(2); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -230,10 +230,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should not sync a deleted album to asset relation when the album is deleted', async () => { @@ -244,7 +245,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -254,11 +254,12 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await albumRepo.delete(album.id); await wait(2); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-user.spec.ts b/server/test/medium/specs/sync/sync-album-user.spec.ts index e3d8a21493..d779ffd9f3 100644 --- a/server/test/medium/specs/sync/sync-album-user.spec.ts +++ b/server/test/medium/specs/sync/sync-album-user.spec.ts @@ -34,6 +34,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); @@ -45,7 +46,6 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -56,10 +56,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync an updated shared user', async () => { @@ -71,11 +72,10 @@ describe(SyncRequestType.AlbumUsersV1, () => { const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.Viewer }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -86,10 +86,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync a deleted shared user', async () => { @@ -100,9 +101,8 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -115,10 +115,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); }); @@ -134,7 +135,6 @@ describe(SyncRequestType.AlbumUsersV1, () => { }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -145,10 +145,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync an updated shared user', async () => { @@ -161,10 +162,14 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(2); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.Viewer }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -178,10 +183,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync a deleted shared user', async () => { @@ -194,10 +200,14 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(2); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.delete({ albumsId: album.id, usersId: user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -210,10 +220,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should backfill album users when a user shares an album with you', async () => { @@ -232,7 +243,6 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -243,6 +253,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial user @@ -285,10 +296,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-album.spec.ts b/server/test/medium/specs/sync/sync-album.spec.ts index 9f44e617e3..591d7e1f3c 100644 --- a/server/test/medium/specs/sync/sync-album.spec.ts +++ b/server/test/medium/specs/sync/sync-album.spec.ts @@ -24,7 +24,6 @@ describe(SyncRequestType.AlbumsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -35,10 +34,11 @@ describe(SyncRequestType.AlbumsV1, () => { }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync a new album', async () => { @@ -46,7 +46,6 @@ describe(SyncRequestType.AlbumsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -55,10 +54,11 @@ describe(SyncRequestType.AlbumsV1, () => { }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album delete', async () => { @@ -67,7 +67,6 @@ describe(SyncRequestType.AlbumsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -76,12 +75,12 @@ describe(SyncRequestType.AlbumsV1, () => { }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await albumRepo.delete(album.id); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -90,10 +89,11 @@ describe(SyncRequestType.AlbumsV1, () => { }, type: SyncEntityType.AlbumDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); describe('shared albums', () => { @@ -104,17 +104,17 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: album.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album share (share before sync)', async () => { @@ -124,17 +124,17 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: album.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album share (share after sync)', async () => { @@ -150,23 +150,24 @@ describe(SyncRequestType.AlbumsV1, () => { data: expect.objectContaining({ id: userAlbum.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.Editor }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user2Album.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album delete`', async () => { @@ -177,24 +178,27 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); await albumRepo.delete(album.id); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: { albumId: album.id }, type: SyncEntityType.AlbumDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album unshare as an album delete', async () => { @@ -205,10 +209,13 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); @@ -218,10 +225,11 @@ describe(SyncRequestType.AlbumsV1, () => { data: { albumId: album.id }, type: SyncEntityType.AlbumDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-asset-exif.spec.ts index 425ea89054..9aae961b0c 100644 --- a/server/test/medium/specs/sync/sync-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-exif.spec.ts @@ -24,7 +24,6 @@ describe(SyncRequestType.AssetExifsV1, () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -57,10 +56,11 @@ describe(SyncRequestType.AssetExifsV1, () => { }, type: SyncEntityType.AssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetExifsV1]); }); it('should only sync asset exif for own user', async () => { @@ -72,7 +72,10 @@ describe(SyncRequestType.AssetExifsV1, () => { const { session } = await ctx.newSession({ userId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetExifsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-face.spec.ts b/server/test/medium/specs/sync/sync-asset-face.spec.ts index 68d3007c52..8b4310e600 100644 --- a/server/test/medium/specs/sync/sync-asset-face.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-face.spec.ts @@ -26,7 +26,6 @@ describe(SyncEntityType.AssetFaceV1, () => { const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -44,10 +43,11 @@ describe(SyncEntityType.AssetFaceV1, () => { }), type: 'AssetFaceV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); it('should detect and sync a deleted asset face', async () => { @@ -58,7 +58,6 @@ describe(SyncEntityType.AssetFaceV1, () => { await personRepo.deleteAssetFace(assetFace.id); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -67,10 +66,11 @@ describe(SyncEntityType.AssetFaceV1, () => { }, type: 'AssetFaceDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); it('should not sync an asset face or asset face delete for an unrelated user', async () => { @@ -82,11 +82,18 @@ describe(SyncEntityType.AssetFaceV1, () => { const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); await personRepo.deleteAssetFace(assetFace.id); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-metadata.spec.ts b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts new file mode 100644 index 0000000000..8ba9630520 --- /dev/null +++ b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts @@ -0,0 +1,128 @@ +import { Kysely } from 'kysely'; +import { AssetMetadataKey, SyncEntityType, SyncRequestType } from 'src/enum'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { DB } from 'src/schema'; +import { SyncTestContext } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.AssetMetadataV1, () => { + it('should detect and sync new asset metadata', async () => { + const { auth, user, ctx } = await setup(); + + const assetRepo = ctx.get(AssetRepository); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + key: AssetMetadataKey.MobileApp, + assetId: asset.id, + value: { iCloudId: 'abc123' }, + }, + type: 'AssetMetadataV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetMetadataV1]); + }); + + it('should update asset metadata', async () => { + const { auth, user, ctx } = await setup(); + + const assetRepo = ctx.get(AssetRepository); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + key: AssetMetadataKey.MobileApp, + assetId: asset.id, + value: { iCloudId: 'abc123' }, + }, + type: 'AssetMetadataV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + + await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc456' } }]); + + const updatedResponse = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); + expect(updatedResponse).toEqual([ + { + ack: expect.any(String), + data: { + key: AssetMetadataKey.MobileApp, + assetId: asset.id, + value: { iCloudId: 'abc456' }, + }, + type: 'AssetMetadataV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, updatedResponse); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetMetadataV1]); + }); +}); + +describe(SyncEntityType.AssetMetadataDeleteV1, () => { + it('should delete and sync asset metadata', async () => { + const { auth, user, ctx } = await setup(); + + const assetRepo = ctx.get(AssetRepository); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + key: AssetMetadataKey.MobileApp, + assetId: asset.id, + value: { iCloudId: 'abc123' }, + }, + type: 'AssetMetadataV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + + await assetRepo.deleteMetadataByKey(asset.id, AssetMetadataKey.MobileApp); + + await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + key: AssetMetadataKey.MobileApp, + }, + type: 'AssetMetadataDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + }); +}); diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index ce83eed98c..066cb2de4d 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -40,7 +40,6 @@ describe(SyncEntityType.AssetV1, () => { }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -64,10 +63,11 @@ describe(SyncEntityType.AssetV1, () => { }, type: 'AssetV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); it('should detect and sync a deleted asset', async () => { @@ -77,7 +77,6 @@ describe(SyncEntityType.AssetV1, () => { await assetRepo.remove(asset); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -86,10 +85,11 @@ describe(SyncEntityType.AssetV1, () => { }, type: 'AssetDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); it('should not sync an asset or asset delete for an unrelated user', async () => { @@ -100,11 +100,17 @@ describe(SyncEntityType.AssetV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); await assetRepo.remove(asset); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-auth-user.spec.ts b/server/test/medium/specs/sync/sync-auth-user.spec.ts index 80ce8b37fa..43411129d3 100644 --- a/server/test/medium/specs/sync/sync-auth-user.spec.ts +++ b/server/test/medium/specs/sync/sync-auth-user.spec.ts @@ -22,7 +22,6 @@ describe(SyncEntityType.AuthUserV1, () => { const { auth, user, ctx } = await setup(await getKyselyDB()); const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -43,10 +42,11 @@ describe(SyncEntityType.AuthUserV1, () => { }, type: 'AuthUserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AuthUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AuthUsersV1]); }); it('should sync a change and then another change to that same user', async () => { @@ -55,7 +55,6 @@ describe(SyncEntityType.AuthUserV1, () => { const userRepo = ctx.get(UserRepository); const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -65,6 +64,7 @@ describe(SyncEntityType.AuthUserV1, () => { }), type: 'AuthUserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -72,7 +72,6 @@ describe(SyncEntityType.AuthUserV1, () => { await userRepo.update(user.id, { isAdmin: true }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -82,6 +81,26 @@ describe(SyncEntityType.AuthUserV1, () => { }), type: 'AuthUserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + }); + + it('should only sync the auth user', async () => { + const { auth, user, ctx } = await setup(await getKyselyDB()); + + await ctx.newUser(); + + const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: user.id, + isAdmin: false, + }), + type: 'AuthUserV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-complete.spec.ts b/server/test/medium/specs/sync/sync-complete.spec.ts new file mode 100644 index 0000000000..8a94061631 --- /dev/null +++ b/server/test/medium/specs/sync/sync-complete.spec.ts @@ -0,0 +1,60 @@ +import { Kysely } from 'kysely'; +import { DateTime } from 'luxon'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository'; +import { DB } from 'src/schema'; +import { toAck } from 'src/utils/sync'; +import { SyncTestContext } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; +import { v7 } from 'uuid'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.SyncCompleteV1, () => { + it('should work', async () => { + const { auth, ctx } = await setup(); + + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); + }); + + it('should detect an old checkpoint and send back a reset', async () => { + const { auth, session, ctx } = await setup(); + const updateId = v7({ msecs: DateTime.now().minus({ days: 60 }).toMillis() }); + + await ctx.get(SyncCheckpointRepository).upsertAll([ + { + type: SyncEntityType.SyncCompleteV1, + sessionId: session.id, + ack: toAck({ type: SyncEntityType.SyncCompleteV1, updateId }), + }, + ]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); + expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' }]); + }); + + it('should not send back a reset if the checkpoint is recent', async () => { + const { auth, session, ctx } = await setup(); + const updateId = v7({ msecs: DateTime.now().minus({ days: 7 }).toMillis() }); + + await ctx.get(SyncCheckpointRepository).upsertAll([ + { + type: SyncEntityType.SyncCompleteV1, + sessionId: session.id, + ack: toAck({ type: SyncEntityType.SyncCompleteV1, updateId }), + }, + ]); + + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); + }); +}); diff --git a/server/test/medium/specs/sync/sync-memory-asset.spec.ts b/server/test/medium/specs/sync/sync-memory-asset.spec.ts index a3247637d7..f0cae0934e 100644 --- a/server/test/medium/specs/sync/sync-memory-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-memory-asset.spec.ts @@ -25,7 +25,6 @@ describe(SyncEntityType.MemoryToAssetV1, () => { await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -35,10 +34,11 @@ describe(SyncEntityType.MemoryToAssetV1, () => { }, type: 'MemoryToAssetV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); }); it('should detect and sync a deleted memory to asset relation', async () => { @@ -50,7 +50,6 @@ describe(SyncEntityType.MemoryToAssetV1, () => { await memoryRepo.removeAssetIds(memory.id, [asset.id]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -60,10 +59,11 @@ describe(SyncEntityType.MemoryToAssetV1, () => { }, type: 'MemoryToAssetDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); }); it('should not sync a memory to asset relation or delete for an unrelated user', async () => { @@ -74,11 +74,18 @@ describe(SyncEntityType.MemoryToAssetV1, () => { const { memory } = await ctx.newMemory({ ownerId: user2.id }); await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); - expect(await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(0); - expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1); + expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.MemoryToAssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); await memoryRepo.removeAssetIds(memory.id, [asset.id]); - expect(await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(0); - expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1); + + expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.MemoryToAssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-memory.spec.ts b/server/test/medium/specs/sync/sync-memory.spec.ts index fa833ad094..1889f39626 100644 --- a/server/test/medium/specs/sync/sync-memory.spec.ts +++ b/server/test/medium/specs/sync/sync-memory.spec.ts @@ -23,7 +23,6 @@ describe(SyncEntityType.MemoryV1, () => { const { memory } = await ctx.newMemory({ ownerId: user1.id }); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -43,10 +42,11 @@ describe(SyncEntityType.MemoryV1, () => { }, type: 'MemoryV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); it('should detect and sync a deleted memory', async () => { @@ -56,7 +56,6 @@ describe(SyncEntityType.MemoryV1, () => { await memoryRepo.delete(memory.id); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -65,10 +64,11 @@ describe(SyncEntityType.MemoryV1, () => { }, type: 'MemoryDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); it('should sync a memory and then an update to that same memory', async () => { @@ -77,29 +77,29 @@ describe(SyncEntityType.MemoryV1, () => { const { memory } = await ctx.newMemory({ ownerId: user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: memory.id }), type: 'MemoryV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await memoryRepo.update(memory.id, { seenAt: new Date() }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: memory.id }), type: 'MemoryV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); it('should not sync a memory or a memory delete for an unrelated user', async () => { @@ -108,8 +108,8 @@ describe(SyncEntityType.MemoryV1, () => { const { user: user2 } = await ctx.newUser(); const { memory } = await ctx.newMemory({ ownerId: user2.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); await memoryRepo.delete(memory.id); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); }); 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 index c33eb59dbb..d44c088f17 100644 --- a/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts @@ -26,7 +26,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -59,10 +58,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }, type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should not sync partner asset exif for own user', async () => { @@ -72,8 +72,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); await ctx.newExif({ assetId: asset.id, make: 'Canon' }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should not sync partner asset exif for unrelated user', async () => { @@ -86,8 +89,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should backfill partner asset exif when a partner shared their library with you', async () => { @@ -102,7 +108,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual( expect.arrayContaining([ { @@ -112,6 +117,7 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]), ); @@ -119,7 +125,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -133,10 +138,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should handle partners with users ids lower than a uuidv7', async () => { @@ -151,7 +157,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -160,15 +165,15 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); // This checks that our ack upsert is correct - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), @@ -182,10 +187,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { @@ -203,7 +209,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -212,13 +217,13 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(newResponse).toHaveLength(3); expect(newResponse).toEqual([ { ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), @@ -239,9 +244,10 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index e9dc7403bd..c30cfcf6bd 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -46,7 +46,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -70,10 +69,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }, type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should detect and sync a deleted partner asset', async () => { @@ -86,7 +86,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await assetRepo.remove(asset); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -95,10 +94,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }, type: SyncEntityType.PartnerAssetDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync a deleted partner asset due to a user delete', async () => { @@ -109,7 +109,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await ctx.newAsset({ ownerId: user2.id }); await userRepo.delete({ id: user2.id }, true); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { @@ -119,9 +119,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => { const { user: user2 } = await ctx.newUser(); await ctx.newAsset({ ownerId: user2.id }); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.PartnerAssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await partnerRepo.remove(partner); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync an asset or asset delete for own user', async () => { @@ -132,13 +135,19 @@ describe(SyncRequestType.PartnerAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); await assetRepo.remove(asset); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync an asset or asset delete for unrelated user', async () => { @@ -150,13 +159,19 @@ describe(SyncRequestType.PartnerAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); await assetRepo.remove(asset); - await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should backfill partner assets when a partner shared their library with you', async () => { @@ -170,7 +185,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -179,13 +193,13 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }), type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -199,10 +213,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { @@ -218,7 +233,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -227,12 +241,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }), type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(newResponse).toHaveLength(3); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -253,9 +267,10 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }), type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-stack.spec.ts b/server/test/medium/specs/sync/sync-partner-stack.spec.ts index 3a879cb580..e1d8416799 100644 --- a/server/test/medium/specs/sync/sync-partner-stack.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-stack.spec.ts @@ -29,7 +29,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -42,10 +41,11 @@ describe(SyncRequestType.PartnerStacksV1, () => { }, type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should detect and sync a deleted partner stack', async () => { @@ -58,7 +58,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { await stackRepo.delete(stack.id); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('PartnerStackDeleteV1'), @@ -67,10 +66,11 @@ describe(SyncRequestType.PartnerStacksV1, () => { }, type: SyncEntityType.PartnerStackDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a deleted partner stack due to a user delete', async () => { @@ -81,7 +81,7 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); await ctx.newStack({ ownerId: user2.id }, [asset.id]); await userRepo.delete({ id: user2.id }, true); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a deleted partner stack due to a partner delete (unshare)', async () => { @@ -91,9 +91,12 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); await ctx.newStack({ ownerId: user2.id }, [asset.id]); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.PartnerStackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await partnerRepo.remove(partner); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a stack or stack delete for own user', async () => { @@ -103,11 +106,17 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { asset } = await ctx.newAsset({ ownerId: user.id }); const { stack } = await ctx.newStack({ ownerId: user.id }, [asset.id]); await ctx.newPartner({ sharedById: user2.id, sharedWithId: user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); await stackRepo.delete(stack.id); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a stack or stack delete for unrelated user', async () => { @@ -119,13 +128,19 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]); const auth2 = factory.auth({ session, user: user2 }); - await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); await stackRepo.delete(stack.id); - await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should backfill partner stacks when a partner shared their library with you', async () => { @@ -140,7 +155,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('PartnerStackV1'), @@ -149,12 +163,12 @@ describe(SyncRequestType.PartnerStacksV1, () => { }), type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.stringContaining(SyncEntityType.PartnerStackBackfillV1), @@ -168,10 +182,11 @@ describe(SyncRequestType.PartnerStacksV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should only backfill partner stacks created prior to the current partner stack checkpoint', async () => { @@ -189,7 +204,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining(SyncEntityType.PartnerStackV1), @@ -198,12 +212,12 @@ describe(SyncRequestType.PartnerStacksV1, () => { }), type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(newResponse).toHaveLength(3); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -224,9 +238,10 @@ describe(SyncRequestType.PartnerStacksV1, () => { }), type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner.spec.ts b/server/test/medium/specs/sync/sync-partner.spec.ts index d20970da8f..19c386070a 100644 --- a/server/test/medium/specs/sync/sync-partner.spec.ts +++ b/server/test/medium/specs/sync/sync-partner.spec.ts @@ -26,7 +26,6 @@ describe(SyncEntityType.PartnerV1, () => { const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -37,10 +36,11 @@ describe(SyncEntityType.PartnerV1, () => { }, type: 'PartnerV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should detect and sync a deleted partner', async () => { @@ -53,22 +53,20 @@ describe(SyncEntityType.PartnerV1, () => { await partnerRepo.remove(partner); const response = await ctx.syncStream(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', + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, }, - ]), - ); + type: 'PartnerDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should detect and sync a partner share both to and from another user', async () => { @@ -79,32 +77,30 @@ describe(SyncEntityType.PartnerV1, () => { const { partner: partner2 } = await ctx.newPartner({ sharedById: user1.id, sharedWithId: user2.id }); const response = await ctx.syncStream(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', + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + inTimeline: partner1.inTimeline, + sharedById: partner1.sharedById, + sharedWithId: partner1.sharedWithId, }, - { - ack: expect.any(String), - data: { - inTimeline: partner2.inTimeline, - sharedById: partner2.sharedById, - sharedWithId: partner2.sharedWithId, - }, - type: 'PartnerV1', + type: 'PartnerV1', + }, + { + ack: expect.any(String), + data: { + inTimeline: partner2.inTimeline, + sharedById: partner2.sharedById, + sharedWithId: partner2.sharedWithId, }, - ]), - ); + type: 'PartnerV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should sync a partner and then an update to that same partner', async () => { @@ -116,7 +112,6 @@ describe(SyncEntityType.PartnerV1, () => { const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -127,6 +122,7 @@ describe(SyncEntityType.PartnerV1, () => { }, type: 'PartnerV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -137,7 +133,6 @@ describe(SyncEntityType.PartnerV1, () => { ); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -148,10 +143,11 @@ describe(SyncEntityType.PartnerV1, () => { }, type: 'PartnerV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should not sync a partner or partner delete for an unrelated user', async () => { @@ -163,9 +159,9 @@ describe(SyncEntityType.PartnerV1, () => { const { user: user3 } = await ctx.newUser(); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user3.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); await partnerRepo.remove(partner); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should not sync a partner delete after a user is deleted', async () => { @@ -177,6 +173,6 @@ describe(SyncEntityType.PartnerV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await userRepo.delete({ id: user2.id }, true); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-person.spec.ts b/server/test/medium/specs/sync/sync-person.spec.ts index fbf401e377..6fdb5a58f2 100644 --- a/server/test/medium/specs/sync/sync-person.spec.ts +++ b/server/test/medium/specs/sync/sync-person.spec.ts @@ -24,7 +24,6 @@ describe(SyncEntityType.PersonV1, () => { const { person } = await ctx.newPerson({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -40,10 +39,11 @@ describe(SyncEntityType.PersonV1, () => { }), type: 'PersonV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PeopleV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); }); it('should detect and sync a deleted person', async () => { @@ -53,7 +53,6 @@ describe(SyncEntityType.PersonV1, () => { await personRepo.delete([person.id]); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -62,10 +61,11 @@ describe(SyncEntityType.PersonV1, () => { }, type: 'PersonDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PeopleV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); }); it('should not sync a person or person delete for an unrelated user', async () => { @@ -76,11 +76,18 @@ describe(SyncEntityType.PersonV1, () => { const { person } = await ctx.newPerson({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.PersonV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); await personRepo.delete([person.id]); - expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); + + expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.PersonDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-reset.spec.ts b/server/test/medium/specs/sync/sync-reset.spec.ts index 699c5dc292..9a4c33c1f2 100644 --- a/server/test/medium/specs/sync/sync-reset.spec.ts +++ b/server/test/medium/specs/sync/sync-reset.spec.ts @@ -21,8 +21,7 @@ describe(SyncEntityType.SyncResetV1, () => { it('should work', async () => { const { auth, ctx } = await setup(); - const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); - expect(response).toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); it('should detect a pending sync reset', async () => { @@ -41,7 +40,10 @@ describe(SyncEntityType.SyncResetV1, () => { await ctx.newAsset({ ownerId: user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.get(SessionRepository).update(auth.session!.id, { isPendingSyncReset: true, @@ -62,9 +64,8 @@ describe(SyncEntityType.SyncResetV1, () => { }); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([ - expect.objectContaining({ - type: SyncEntityType.AssetV1, - }), + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); @@ -86,9 +87,8 @@ describe(SyncEntityType.SyncResetV1, () => { const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); expect(postResetResponse).toEqual([ - expect.objectContaining({ - type: SyncEntityType.AssetV1, - }), + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-stack.spec.ts b/server/test/medium/specs/sync/sync-stack.spec.ts index 1696172911..d3304ded28 100644 --- a/server/test/medium/specs/sync/sync-stack.spec.ts +++ b/server/test/medium/specs/sync/sync-stack.spec.ts @@ -25,7 +25,6 @@ describe(SyncEntityType.StackV1, () => { const { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('StackV1'), @@ -38,10 +37,11 @@ describe(SyncEntityType.StackV1, () => { }, type: 'StackV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); it('should detect and sync a deleted stack', async () => { @@ -53,17 +53,17 @@ describe(SyncEntityType.StackV1, () => { await stackRepo.delete(stack.id); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('StackDeleteV1'), data: { stackId: stack.id }, type: 'StackDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); it('should sync a stack and then an update to that same stack', async () => { @@ -74,22 +74,29 @@ describe(SyncEntityType.StackV1, () => { const { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(response).toHaveLength(1); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); await stackRepo.update(stack.id, { primaryAssetId: asset2.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); expect(newResponse).toEqual([ { ack: expect.stringContaining('StackV1'), data: expect.objectContaining({ id: stack.id, primaryAssetId: asset2.id }), type: 'StackV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); it('should not sync a stack or stack delete for an unrelated user', async () => { @@ -100,8 +107,8 @@ describe(SyncEntityType.StackV1, () => { const { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id }); const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset1.id, asset2.id]); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); await stackRepo.delete(stack.id); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-user-metadata.spec.ts b/server/test/medium/specs/sync/sync-user-metadata.spec.ts index 7cd53e76e3..1e75f80194 100644 --- a/server/test/medium/specs/sync/sync-user-metadata.spec.ts +++ b/server/test/medium/specs/sync/sync-user-metadata.spec.ts @@ -25,7 +25,6 @@ describe(SyncEntityType.UserMetadataV1, () => { await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -36,10 +35,11 @@ describe(SyncEntityType.UserMetadataV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UserMetadataV1]); }); it('should update user metadata', async () => { @@ -49,7 +49,6 @@ describe(SyncEntityType.UserMetadataV1, () => { await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -60,6 +59,7 @@ describe(SyncEntityType.UserMetadataV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -77,10 +77,11 @@ describe(SyncEntityType.UserMetadataV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, updatedResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UserMetadataV1]); }); }); @@ -92,7 +93,6 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -103,6 +103,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -118,6 +119,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { }, type: 'UserMetadataDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-user.spec.ts b/server/test/medium/specs/sync/sync-user.spec.ts index c5d572d7d6..7a69e7a411 100644 --- a/server/test/medium/specs/sync/sync-user.spec.ts +++ b/server/test/medium/specs/sync/sync-user.spec.ts @@ -28,7 +28,6 @@ describe(SyncEntityType.UserV1, () => { } const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -43,10 +42,11 @@ describe(SyncEntityType.UserV1, () => { }, type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UsersV1]); }); it('should detect and sync a soft deleted user', async () => { @@ -56,7 +56,6 @@ describe(SyncEntityType.UserV1, () => { const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(2); expect(response).toEqual( expect.arrayContaining([ { @@ -69,11 +68,12 @@ describe(SyncEntityType.UserV1, () => { data: expect.objectContaining({ id: deleted.id }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]), ); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UsersV1]); }); it('should detect and sync a deleted user', async () => { @@ -85,7 +85,6 @@ describe(SyncEntityType.UserV1, () => { await userRepo.delete({ id: user.id }, true); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ { ack: expect.any(String), @@ -99,10 +98,11 @@ describe(SyncEntityType.UserV1, () => { data: expect.objectContaining({ id: authUser.id }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UsersV1]); }); it('should sync a user and then an update to that same user', async () => { @@ -111,13 +111,13 @@ describe(SyncEntityType.UserV1, () => { const userRepo = ctx.get(UserRepository); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user.id }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -125,13 +125,13 @@ describe(SyncEntityType.UserV1, () => { const updated = await userRepo.update(auth.user.id, { name: 'new name' }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user.id, name: updated.name }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 26ef9dcf9c..e853deb270 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -42,5 +42,9 @@ export const newAssetRepositoryMock = (): Mocked randomUUID() as string; +export const newUuid = () => v4(); export const newUuids = () => Array.from({ length: 100 }) .fill(0) .map(() => newUuid()); export const newDate = () => new Date(); -export const newUuidV7 = () => 'uuid-v7'; +export const newUuidV7 = () => v7(); export const newSha1 = () => Buffer.from('this is a fake hash'); export const newEmbedding = () => { const embedding = Array.from({ length: 512 }) diff --git a/server/test/utils.ts b/server/test/utils.ts index 95f9741d7c..c23341d64c 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,12 +1,16 @@ -import { CallHandler, Provider, ValidationPipe } from '@nestjs/common'; +import { CallHandler, ExecutionContext, Provider, ValidationPipe } from '@nestjs/common'; import { APP_GUARD, APP_PIPE } from '@nestjs/core'; +import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; import { Test } from '@nestjs/testing'; import { ClassConstructor } from 'class-transformer'; +import { NextFunction } from 'express'; import { Kysely } from 'kysely'; +import multer from 'multer'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Readable, Writable } from 'node:stream'; import { PNG } from 'pngjs'; import postgres from 'postgres'; +import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AuthGuard } from 'src/middleware/auth.guard'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; @@ -82,6 +86,24 @@ export type ControllerContext = { export const controllerSetup = async (controller: ClassConstructor, providers: Provider[]) => { const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; + const upload = multer({ storage: multer.memoryStorage() }); + const memoryFileInterceptor = { + intercept: async (ctx: ExecutionContext, next: CallHandler) => { + const context = ctx.switchToHttp(); + const handler = upload.fields([ + { name: UploadFieldName.ASSET_DATA, maxCount: 1 }, + { name: UploadFieldName.SIDECAR_DATA, maxCount: 1 }, + ]); + + await new Promise((resolve, reject) => { + const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve()); + const maybePromise = handler(context.getRequest(), context.getResponse(), next); + Promise.resolve(maybePromise).catch((error) => reject(error)); + }); + + return next.handle(); + }, + }; const moduleRef = await Test.createTestingModule({ controllers: [controller], providers: [ @@ -93,7 +115,7 @@ export const controllerSetup = async (controller: ClassConstructor, pro ], }) .overrideInterceptor(FileUploadInterceptor) - .useValue(noopInterceptor) + .useValue(memoryFileInterceptor) .overrideInterceptor(AssetUploadInterceptor) .useValue(noopInterceptor) .compile(); diff --git a/web/.nvmrc b/web/.nvmrc index 91d5f6ff8e..442c7587a9 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -22.18.0 +22.20.0 diff --git a/web/bin/immich-web b/web/bin/immich-web index 29e7c46edd..23377eddd6 100755 --- a/web/bin/immich-web +++ b/web/bin/immich-web @@ -1,8 +1,9 @@ #!/usr/bin/env sh -echo "Build dependencies for Immich Web" cd /usr/src/app || exit +pnpm --filter @immich/sdk build + COUNT=0 UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}" UPSTREAM="${UPSTREAM%/}" @@ -14,5 +15,4 @@ until wget --spider --quiet "${UPSTREAM}/api/server/config" > /dev/null 2>&1; do sleep 1 done echo "Connected to $UPSTREAM, starting Immich Web..." -pnpm --filter @immich/sdk build pnpm --filter immich-web exec vite dev --host 0.0.0.0 --port 3000 diff --git a/web/eslint.config.js b/web/eslint.config.js index e15f80e8e0..d2ef7631e7 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -1,5 +1,6 @@ import js from '@eslint/js'; import tslintPluginCompat from '@koddsson/eslint-plugin-tscompat'; +import prettier from 'eslint-config-prettier'; import eslintPluginCompat from 'eslint-plugin-compat'; import eslintPluginSvelte from 'eslint-plugin-svelte'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; @@ -17,6 +18,7 @@ export default typescriptEslint.config( ...eslintPluginSvelte.configs.recommended, eslintPluginUnicorn.configs.recommended, js.configs.recommended, + prettier, { plugins: { tscompat: tslintPluginCompat, @@ -125,6 +127,7 @@ export default typescriptEslint.config( '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/require-await': 'error', 'object-shorthand': ['error', 'always'], + 'svelte/no-navigation-without-resolve': 'off', }, }, { diff --git a/web/package.json b/web/package.json index dffc246da2..e7edd4e108 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.139.4", + "version": "2.0.0", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { @@ -9,7 +9,7 @@ "build:stats": "BUILD_STATS=true vite build", "package": "svelte-kit package", "preview": "vite preview", - "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte", + "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings", "check:typescript": "tsc --noEmit", "check:watch": "npm run check:svelte -- --watch", "check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript", @@ -28,7 +28,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.24.0", + "@immich/ui": "^0.29.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -53,8 +53,9 @@ "maplibre-gl": "^5.6.2", "pmtiles": "^4.3.0", "qrcode": "^1.5.4", + "simple-icons": "^15.15.0", "socket.io-client": "~4.8.0", - "svelte-gestures": "^5.1.3", + "svelte-gestures": "^5.2.2", "svelte-i18n": "^4.0.1", "svelte-maplibre": "^1.2.0", "svelte-persisted-store": "^0.12.0", @@ -62,15 +63,14 @@ "thumbhash": "^0.1.1" }, "devDependencies": { - "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.18.0", - "@faker-js/faker": "^9.3.0", + "@faker-js/faker": "^10.0.0", "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.8.0", "@sveltejs/kit": "^2.27.1", - "@sveltejs/vite-plugin-svelte": "6.1.2", + "@sveltejs/vite-plugin-svelte": "6.2.0", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", @@ -82,11 +82,10 @@ "@types/luxon": "^3.4.2", "@types/qrcode": "^1.5.5", "@vitest/coverage-v8": "^3.0.0", - "autoprefixer": "^10.4.17", "dotenv": "^17.0.0", "eslint": "^9.18.0", "eslint-config-prettier": "^10.1.8", - "eslint-p": "^0.25.0", + "eslint-p": "^0.26.0", "eslint-plugin-compat": "^6.0.2", "eslint-plugin-svelte": "^3.9.0", "eslint-plugin-unicorn": "^60.0.0", @@ -98,17 +97,16 @@ "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", "rollup-plugin-visualizer": "^6.0.0", - "svelte": "5.35.5", + "svelte": "5.38.10", "svelte-check": "^4.1.5", "svelte-eslint-parser": "^1.2.0", "tailwindcss": "^4.1.7", - "tslib": "^2.6.2", "typescript": "^5.8.3", "typescript-eslint": "^8.28.0", "vite": "^7.1.2", "vitest": "^3.0.0" }, "volta": { - "node": "22.18.0" + "node": "22.20.0" } } diff --git a/web/src/app.css b/web/src/app.css index db6c43652b..f66743f736 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -169,3 +169,13 @@ filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.8)); } } + +.maplibregl-popup { + .maplibregl-popup-tip { + @apply border-t-subtle! translate-y-[-1px]; + } + + .maplibregl-popup-content { + @apply bg-subtle rounded-lg; + } +} diff --git a/web/src/app.d.ts b/web/src/app.d.ts index d0d25443c9..e0631e3617 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -36,7 +36,7 @@ type NestedKeys = K extends keyof T & string : never; declare module 'svelte-i18n' { - import type { InterpolationValues } from '$lib/components/i18n/format-message.svelte'; + import type { InterpolationValues } from '$lib/elements/format-message.svelte'; import type { Readable } from 'svelte/store'; type Translations = NestedKeys; diff --git a/web/src/lib/assets/empty-5.svg b/web/src/lib/assets/empty-5.svg new file mode 100644 index 0000000000..e9e24d0499 --- /dev/null +++ b/web/src/lib/assets/empty-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/lib/assets/svg-paths.ts b/web/src/lib/assets/svg-paths.ts index ded3db0fc8..cc8d0a1800 100644 --- a/web/src/lib/assets/svg-paths.ts +++ b/web/src/lib/assets/svg-paths.ts @@ -4,7 +4,3 @@ export const sunPath = export const moonViewBox = '0 0 20 20'; export const sunViewBox = '0 0 20 20'; - -export const discordPath = - 'M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z'; -export const discordViewBox = '0 0 126.644 96'; diff --git a/web/src/lib/components/ServerAboutItem.svelte b/web/src/lib/components/ServerAboutItem.svelte new file mode 100644 index 0000000000..9e169a9839 --- /dev/null +++ b/web/src/lib/components/ServerAboutItem.svelte @@ -0,0 +1,24 @@ + + +
+ + + {#if versionHref} + {version} + {:else} + {version} + {/if} + +
diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-settings/AdminSettings.svelte similarity index 100% rename from web/src/lib/components/admin-page/settings/admin-settings.svelte rename to web/src/lib/components/admin-settings/AdminSettings.svelte diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-settings/AuthSettings.svelte similarity index 92% rename from web/src/lib/components/admin-page/settings/auth/auth-settings.svelte rename to web/src/lib/components/admin-settings/AuthSettings.svelte index ef371910c5..dbc96ac02a 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-settings/AuthSettings.svelte @@ -1,5 +1,4 @@
-

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

+

{$t('date_and_time')}

@@ -27,7 +27,7 @@
-

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

+

{$t('year')}

    {#each options.yearOptions as yearFormat, index (index)}
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • @@ -36,7 +36,7 @@
-

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

+

{$t('month')}

    {#each options.monthOptions as monthFormat, index (index)}
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • @@ -45,7 +45,7 @@
-

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

+

{$t('week')}

    {#each options.weekOptions as weekFormat, index (index)}
  • {'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}
  • @@ -54,7 +54,7 @@
-

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

+

{$t('day')}

    {#each options.dayOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • @@ -63,7 +63,7 @@
-

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

+

{$t('hour')}

    {#each options.hourOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • @@ -72,7 +72,7 @@
-

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

+

{$t('minute')}

    {#each options.minuteOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • @@ -81,7 +81,7 @@
-

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

+

{$t('second')}