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 abf34ad68c..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 'find /data -maxdepth 1 ! -path "/data/postgres" -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; 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' 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/.github/.nvmrc b/.github/.nvmrc index 91d5f6ff8e..e2228113dd 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.0 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/close-duplicates.yml b/.github/workflows/close-duplicates.yml index b3411351a3..8470e0e18c 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -35,22 +35,21 @@ jobs: 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 }} - # TODO: We should detect if the checkbox is missing entirely and also close_and_comment in that case. 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, should_run] - if: ${{ needs.should_run.outputs.should_run == 'true' && !fromJSON(needs.get_checkbox_json.outputs.json).items[0].list[0].checked }} + 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/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..bec34c2713 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -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..a0329c8f73 --- /dev/null +++ b/.github/workflows/merge-translations.yml @@ -0,0 +1,112 @@ +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 + +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 + 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 + uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + + - name: Lock weblate + 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 + 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 + 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 + 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 + 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 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..add68ffbd7 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -24,6 +24,15 @@ concurrency: permissions: {} jobs: + merge_translations: + uses: ./.github/workflows/merge-translations.yml + 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 outputs: @@ -110,7 +119,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..e11330d428 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -100,36 +100,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 e3d2c9b0dc..cd02d8341a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -594,7 +594,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..d765db6c1a 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -3,16 +3,25 @@ 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.found_paths.outputs.i18n == 'true' }} steps: - name: Checkout code uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -32,19 +41,15 @@ jobs: permissions: {} if: ${{ needs.pre-job.outputs.should_run == '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..25731cc2aa 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,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 13da918683..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: @@ -68,30 +68,6 @@ VOLUME_DIRS = \ # Include .env file if it exists -include docker/.env -# Helper function to chown, on error suggest remediation and exit -define safe_chown - if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \ - true; \ - else \ - echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \ - exit 1; \ - fi; -endef -# create empty directories and chown -prepare-volumes: - @$(foreach dir,$(VOLUME_DIRS),mkdir -p $(dir);) - @$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R)) -ifneq ($(UPLOAD_LOCATION),) -ifeq ($(filter /%,$(UPLOAD_LOCATION)),) - @mkdir -p "docker/$(UPLOAD_LOCATION)" - @$(call safe_chown,docker/$(UPLOAD_LOCATION),) -else - @mkdir -p "$(UPLOAD_LOCATION)" - @$(call safe_chown,$(UPLOAD_LOCATION),) -endif -endif - - MODULES = e2e server web cli sdk docs .github # directory to package name mapping function diff --git a/cli/.nvmrc b/cli/.nvmrc index 91d5f6ff8e..e2228113dd 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.0 diff --git a/cli/package.json b/cli/package.json index 9697ff4d1d..5b9b2d810c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.86", + "version": "2.2.90", "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.19.0" } } 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/.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/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 372352d12a..f97cf0ca0d 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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 @@ -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:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107 env_file: - .env environment: @@ -183,25 +174,6 @@ services: # volumes: # - grafana-data:/var/lib/grafana - init: - container_name: init - image: busybox@sha256:ab33eacc8251e3807b85bb6dba570e4698c3998eca6f0fc2ccb60575a563ea74 - env_file: - - .env - user: 0:0 - command: sh -c 'find /data -maxdepth 1 -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; 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..c3fb9c7736 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -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:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107 env_file: - .env environment: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 052ae8b334..3316c17839 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} diff --git a/docs/.nvmrc b/docs/.nvmrc index 91d5f6ff8e..e2228113dd 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.0 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/monitoring.md b/docs/docs/features/monitoring.md index 64377ec073..c80f66902b 100644 --- a/docs/docs/features/monitoring.md +++ b/docs/docs/features/monitoring.md @@ -66,7 +66,7 @@ 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). ::: diff --git a/docs/docs/features/tags.md b/docs/docs/features/tags.md index ca663e9edd..a5b6752c81 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](/docs/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/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/database-queries.md b/docs/docs/guides/database-queries.md index 267e7bf2ad..1a5c2ed193 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -147,7 +147,10 @@ SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config'; ### 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/package.json b/docs/package.json index 7fbcbada5a..1a1dbcf84c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,8 +24,6 @@ "@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", @@ -60,6 +58,6 @@ "node": ">=20" }, "volta": { - "node": "22.18.0" + "node": "22.19.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/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/static/archived-versions.json b/docs/static/archived-versions.json index c05615b8cc..8edce0be2d 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,20 @@ [ + { + "label": "v1.142.1", + "url": "https://v1.142.1.archive.immich.app" + }, + { + "label": "v1.142.0", + "url": "https://v1.142.0.archive.immich.app" + }, + { + "label": "v1.141.1", + "url": "https://v1.141.1.archive.immich.app" + }, + { + "label": "v1.141.0", + "url": "https://v1.141.0.archive.immich.app" + }, { "label": "v1.140.1", "url": "https://v1.140.1.archive.immich.app" diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 91d5f6ff8e..e2228113dd 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.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 e4f1591e0e..737f488a50 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.140.1", + "version": "1.142.1", "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.19.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/ar.json b/i18n/ar.json index 367a9e47c1..8a436deba5 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -396,6 +396,7 @@ "advanced_settings_prefer_remote_title": "تفضل الصور البعيدة", "advanced_settings_proxy_headers_subtitle": "عرف عناوين الوكيل التي يستخدمها Immich لارسال كل طلب شبكي", "advanced_settings_proxy_headers_title": "عناوين الوكيل", + "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": "حذف او استعادة تلقائي للاصول على هذا الجهاز عند تنفيذ العملية على الويب", @@ -591,8 +592,6 @@ "backup_setting_subtitle": "ادارة اعدادات التحميل في الخلفية والمقدمة", "backup_settings_subtitle": "إدارة إعدادات التحميل", "backward": "الى الوراء", - "beta_sync": "حالة المزامنة التجريبية", - "beta_sync_subtitle": "ادارة نظام المزامنة الجديد", "biometric_auth_enabled": "المصادقة البايومترية مفعله", "biometric_locked_out": "لقد قفلت عنك المصادقة البيومترية", "biometric_no_options": "لا توجد خيارات بايومترية متوفرة", diff --git a/i18n/az.json b/i18n/az.json index 19ca4aa08d..0449289735 100644 --- a/i18n/az.json +++ b/i18n/az.json @@ -1,37 +1,53 @@ { - "about": "Haqqinda", + "about": "Haqqında", "account": "Hesab", "account_settings": "Hesab parametrləri", "acknowledge": "Təsdiq et", "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_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_settings": "Ehtiyat Nüsxə Parametrləri", "backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et", "config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub", @@ -84,5 +100,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 bf23f4655e..f98609b84b 100644 --- a/i18n/be.json +++ b/i18n/be.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_albums": "Дадаць у альбомы", + "add_to_albums_count": "Дадаць у альбомы ({count})", "add_to_shared_album": "Дадаць у агульны альбом", "add_url": "Дадаць URL", "added_to_archive": "Дададзена ў архіў", @@ -399,6 +401,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..8d9727e492 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -396,6 +396,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": "Автоматично изтрии или възстанови обект на това устройство, когато действието е извършено през уеб-интерфейса", @@ -461,6 +463,7 @@ "app_bar_signout_dialog_title": "Излез от профила", "app_settings": "Настройки ма приложението", "appears_in": "Излиза в", + "apply_count": "Приложи ({count, number})", "archive": "Архив", "archive_action_prompt": "{count} са добавени в Архива", "archive_or_unarchive_photo": "Архивиране или деархивиране на снимка", @@ -500,7 +503,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 {# актива}}", @@ -594,8 +597,6 @@ "backup_setting_subtitle": "Управлявай настройките за архивиране в активен и фонов режим", "backup_settings_subtitle": "Управление на настройките за качване", "backward": "Назад", - "beta_sync": "Статус на бета синхронизацията", - "beta_sync_subtitle": "Управление на новата система за синхронизация", "biometric_auth_enabled": "Включена биометрично удостоверяване", "biometric_locked_out": "Няма достъп до биометрично удостоверяване", "biometric_no_options": "Няма биометрична автентикация", @@ -912,7 +913,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": "Неуспешно запазване на този обект и изтриване на останалите обекти", @@ -1073,12 +1074,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": "Групирай по държава", @@ -1262,6 +1266,7 @@ "main_branch_warning": "Използвате версия за разработчици, силно препоръчваме да използвате официална версия!", "main_menu": "Главно меню", "make": "Марка", + "manage_geolocation": "Управление на местоположенията", "manage_shared_links": "Управление на споделени връзки", "manage_sharing_with_partners": "Управление на споделянето с партньори", "manage_the_app_settings": "Управление на настройките на приложението", @@ -1407,6 +1412,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": "Други", @@ -1508,6 +1515,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 +1554,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 +1562,8 @@ "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 {# елемента}} на нов човек", @@ -1722,6 +1733,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": "Адрес на сървъра", @@ -1941,7 +1953,9 @@ "to_change_password": "Промяна на паролата", "to_favorite": "Любим", "to_login": "Вписване", + "to_multi_select": "за избор на няколко", "to_parent": "Отиди към родителския елемент", + "to_select": "за избор", "to_trash": "Кошче", "toggle_settings": "Превключване на настройките", "total": "Общо", @@ -1991,6 +2005,7 @@ "unstacked_assets_count": "Разкачени {count, plural, one {# елемент} other {# елементи}}", "untagged": "Немаркирани", "up_next": "Следващ", + "update_location_action_prompt": "Обнови координатите на {count} избрани обекта с:", "updated_at": "Обновено", "updated_password": "Паролата е актуализирана", "upload": "Качване", @@ -2057,6 +2072,7 @@ "view_next_asset": "Преглед на следващия файл", "view_previous_asset": "Преглед на предишния файл", "view_qr_code": "Виж QR кода", + "view_similar_photos": "Виж подобни снимки", "view_stack": "Покажи в стек", "view_user": "Виж потребителя", "viewer_remove_from_stack": "Премахване от опашката", 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 00afc6ef01..4d532e0476 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -396,6 +396,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", @@ -461,6 +463,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", @@ -594,8 +597,6 @@ "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", - "beta_sync": "Estat de la sincronització beta", - "beta_sync_subtitle": "Administra el nou sistema de sincronització", "biometric_auth_enabled": "Autentificació biomètrica activada", "biometric_locked_out": "Esteu bloquejats fora de l'autenticació biomètrica", "biometric_no_options": "No hi ha opcions biomètriques disponibles", @@ -1073,12 +1074,15 @@ "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", @@ -1197,7 +1201,7 @@ "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", @@ -1262,6 +1266,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ó", @@ -1407,6 +1412,8 @@ "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", @@ -1508,6 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.", "profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Manera de només lectura activada. Feu doble click a la icona de l'avatar de l'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}", @@ -1546,6 +1554,7 @@ "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ó", @@ -1553,6 +1562,8 @@ "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", "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", @@ -1722,6 +1733,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", @@ -1863,7 +1875,7 @@ "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", @@ -1941,7 +1953,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", @@ -1991,6 +2005,7 @@ "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", @@ -2057,6 +2072,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", diff --git a/i18n/cs.json b/i18n/cs.json index e31a9cba26..e97d7f4211 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Tato funkce načítá externí zdroje z Googlu, aby mohla fungovat.", "general": "Obecné", - "geolocation_instruction_all_have_location": "Všechny položky k tomuto datu již mají údaje o poloze. Zkuste zobrazit všechny položky nebo vyberte jiné datum", "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", - "geolocation_instruction_no_date": "Vyberte datum, abyste mohli spravovat údaje o poloze pro fotografie a videa z daného dne", - "geolocation_instruction_no_photos": "Pro tento den nebyly nalezeny žádné fotografie ani videa. Vyberte jiné datum pro jejich zobrazení", "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", @@ -1417,6 +1412,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í", @@ -1518,7 +1515,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 pouze pro čtení je aktivován. Dvojitým klepnutím na ikonu avatara uživatele režim ukončíte.", + "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}", @@ -1557,6 +1554,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í", @@ -1642,6 +1640,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ů", @@ -1735,7 +1734,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", + "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", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "stiskněte ⇧ pro trvalé odstranění položky", "show_album_options": "Zobrazit možnosti alba", "show_albums": "Zobrazit alba", - "show_all_assets": "Zobrazit všechny položky", "show_all_people": "Zobrazit všechny lidi", "show_and_hide_people": "Zobrazit a skrýt osoby", - "show_assets_without_location": "Zobrazit položky bez polohy", "show_file_location": "Zobrazit umístění souboru", "show_gallery": "Zobrazit galerii", "show_hidden_people": "Zobrazit skryté lidi", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Použít biometrické údaje", "use_current_connection": "použít aktuální připojení", "use_custom_date_range": "Použít vlastní rozsah dat", - "use_this_location": "Klikněte pro použití polohy", "user": "Uživatel", "user_has_been_deleted": "Tento uživatel byl smazán.", "user_id": "ID uživatele", @@ -2077,6 +2076,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", diff --git a/i18n/da.json b/i18n/da.json index 6ce35dac88..c45cf4000d 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": "Anerkendelse", "action": "Handling", "action_common_update": "Opdater", "actions": "Handlinger", @@ -126,13 +126,13 @@ "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", @@ -221,6 +221,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.", @@ -269,6 +271,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", @@ -355,7 +358,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", @@ -390,7 +395,9 @@ "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", @@ -406,6 +413,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", @@ -415,6 +423,7 @@ "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_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", @@ -434,6 +443,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", @@ -453,7 +463,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})", @@ -488,10 +500,12 @@ "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", @@ -508,6 +522,7 @@ "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", @@ -572,13 +587,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", @@ -614,6 +631,7 @@ "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", @@ -645,6 +663,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", @@ -715,6 +734,7 @@ "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", @@ -727,9 +747,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", @@ -737,6 +759,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", @@ -745,6 +768,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", @@ -758,9 +783,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", @@ -771,6 +799,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", @@ -788,6 +817,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ø", @@ -814,8 +844,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", @@ -825,6 +859,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", @@ -843,6 +878,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", @@ -886,6 +922,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", @@ -894,6 +931,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", @@ -979,6 +1017,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", @@ -996,6 +1035,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", @@ -1007,6 +1048,7 @@ "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", @@ -1021,21 +1063,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", @@ -1046,6 +1093,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", @@ -1077,7 +1127,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", @@ -1135,10 +1187,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", @@ -1150,7 +1205,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", @@ -1158,7 +1215,9 @@ "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_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", @@ -1172,6 +1231,7 @@ "locked_folder": "Låst mappe", "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", @@ -1180,7 +1240,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", @@ -1206,6 +1266,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", @@ -1258,6 +1319,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", @@ -1265,6 +1327,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", @@ -1276,6 +1339,9 @@ "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_updated": "Netværkskravene er ændret, backup-køen nulstilles", "networking_settings": "Netværk", "networking_subtitle": "Administrer serverens endepunktindstillinger", "never": "aldrig", @@ -1311,6 +1377,7 @@ "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_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", @@ -1326,6 +1393,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", @@ -1344,14 +1412,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", @@ -1401,7 +1472,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.", @@ -1441,6 +1515,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}", @@ -1476,15 +1551,19 @@ "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", "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", @@ -1507,6 +1586,8 @@ "refreshing_faces": "Opdaterer ansigter", "refreshing_metadata": "Opdaterer metadata", "regenerating_thumbnails": "Regenererer forhåndsvisninger", + "remote": "Eksternt", + "remote_assets": "Eksterne objekter", "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?", @@ -1514,7 +1595,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", @@ -1542,19 +1625,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", @@ -1627,6 +1720,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", @@ -1640,6 +1734,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", @@ -1677,7 +1772,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", @@ -1685,6 +1780,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...", @@ -1706,6 +1802,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", @@ -1731,6 +1828,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.}}", @@ -1780,12 +1878,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", @@ -1795,7 +1895,7 @@ "start_date": "Startdato", "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.", @@ -1805,6 +1905,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", @@ -1814,6 +1915,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", @@ -1824,6 +1929,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", @@ -1850,12 +1956,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", @@ -1869,13 +1978,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", @@ -1891,16 +2003,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", @@ -1909,6 +2028,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", @@ -1929,6 +2049,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", @@ -1947,6 +2068,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", @@ -1954,6 +2076,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", diff --git a/i18n/de.json b/i18n/de.json index d3f641a27c..c6317a25bb 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Diese Funktion lädt externe Quellen von Google, um zu funktionieren.", "general": "Allgemein", - "geolocation_instruction_all_have_location": "Alle Dateien für dieses Daten enthalten bereits Standortangaben. Versuche alle Dateien anzuzeigen oder wähle ein anderes Datum", "geolocation_instruction_location": "Klicke auf eine Datei mit GPS Koordinaten um diesen Standort zu verwenden oder wähle einen Standort direkt auf der Karte", - "geolocation_instruction_no_date": "Wähle ein Datum um die Standortangaben der Fotos und Videos dieses Datums zu verwalten", - "geolocation_instruction_no_photos": "Keine Fotos oder Videos an diesem Datum gefunden. Wähle ein anderes Datum", "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,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. Tippe zweimal auf das Benutzer-Avatar-Symbol, um den Modus zu verlassen.", + "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}", @@ -1550,13 +1547,14 @@ "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", @@ -1642,6 +1640,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", @@ -1735,7 +1734,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", + "selected_gps_coordinates": "Ausgewählte GPS-Koordinaten", "send_message": "Nachricht senden", "send_welcome_email": "Begrüssungsmail senden", "server_endpoint": "Server-Endpunkt", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "Drücke ⇧, um die Datei endgültig zu löschen", "show_album_options": "Album-Optionen anzeigen", "show_albums": "Alben anzeigen", - "show_all_assets": "Alle Dateien anzeigen", "show_all_people": "Alle Personen anzeigen", "show_and_hide_people": "Personen ein- & ausblenden", - "show_assets_without_location": "Zeige Dateien ohne Ortsangabe", "show_file_location": "Dateispeicherort anzeigen", "show_gallery": "Galerie anzeigen", "show_hidden_people": "Ausgeblendete Personen anzeigen", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Biometrie verwenden", "use_current_connection": "aktuelle Verbindung verwenden", "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden", - "use_this_location": "Klicken um Ort zu verwenden", "user": "Nutzer", "user_has_been_deleted": "Dieser Benutzer wurde gelöscht.", "user_id": "Nutzer-ID", @@ -2077,6 +2076,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", diff --git a/i18n/el.json b/i18n/el.json index 1394c24ec9..2dff77a48f 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -594,8 +594,6 @@ "backup_setting_subtitle": "Διαχείριση ρυθμίσεων μεταφόρτωσης στο παρασκήνιο και στο προσκήνιο", "backup_settings_subtitle": "Διαχείριση των ρυθμίσεων μεταφόρτωσης", "backward": "Προς τα πίσω", - "beta_sync": "Κατάσταση Συγχρονισμού Beta (δοκιμαστική)", - "beta_sync_subtitle": "Διαχείριση του νέου συστήματος συγχρονισμού", "biometric_auth_enabled": "Βιομετρική ταυτοποίηση ενεργοποιήθηκε", "biometric_locked_out": "Είστε κλειδωμένοι εκτός της βιομετρικής ταυτοποίησης", "biometric_no_options": "Δεν υπάρχουν διαθέσιμοι τρόποι βιομετρικής ταυτοποίησης", diff --git a/i18n/en.json b/i18n/en.json index 0e2737f14a..09e106c512 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -387,8 +387,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}", @@ -425,6 +423,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}", @@ -496,6 +495,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", @@ -529,8 +530,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", @@ -538,6 +541,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…", @@ -597,8 +601,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", @@ -656,6 +658,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.", @@ -742,6 +746,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", @@ -891,7 +896,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", @@ -1056,6 +1063,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", @@ -1076,10 +1084,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", - "geolocation_instruction_all_have_location": "All assets for this date already have location data. Try showing all assets or select a different date", "geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map", - "geolocation_instruction_no_date": "Select a date to manage location data for photos and videos from that day", - "geolocation_instruction_no_photos": "No photos or videos found for this date. Select a different date to show them", "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", @@ -1223,6 +1228,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", @@ -1234,6 +1240,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}", @@ -1264,6 +1271,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", @@ -1306,6 +1314,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", @@ -1346,6 +1355,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", @@ -1356,6 +1366,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", @@ -1369,20 +1380,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", @@ -1417,6 +1433,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", @@ -1518,7 +1536,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. Double-tap the user avatar icon to exit.", + "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}", @@ -1557,6 +1575,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", @@ -1590,6 +1609,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?", @@ -1642,6 +1662,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", @@ -1735,7 +1756,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", + "selected_gps_coordinates": "Selected GPS Coordinates", "send_message": "Send message", "send_welcome_email": "Send welcome email", "server_endpoint": "Server Endpoint", @@ -1848,10 +1869,8 @@ "shift_to_permanent_delete": "press ⇧ to permanently delete asset", "show_album_options": "Show album options", "show_albums": "Show albums", - "show_all_assets": "Show all assets", "show_all_people": "Show all people", "show_and_hide_people": "Show & hide people", - "show_assets_without_location": "Show assets without location", "show_file_location": "Show file location", "show_gallery": "Show gallery", "show_hidden_people": "Show hidden people", @@ -1868,6 +1887,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", @@ -1922,6 +1942,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", @@ -1981,6 +2003,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", @@ -2036,7 +2059,6 @@ "use_biometric": "Use biometric", "use_current_connection": "use current connection", "use_custom_date_range": "Use custom date range instead", - "use_this_location": "Click to use location", "user": "User", "user_has_been_deleted": "This user has been deleted.", "user_id": "User ID", @@ -2079,6 +2101,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", @@ -2097,5 +2120,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 1a0a4e8115..cd37c89315 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -38,7 +38,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", @@ -167,8 +167,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", @@ -232,7 +232,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", @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidad carga recursos externos desde Google para poder funcionar.", "general": "General", - "geolocation_instruction_all_have_location": "Todos los assets de esta fecha ya tienen datos de ubicación. Prueba mostrando todos los assets o selecciona otra fecha", "geolocation_instruction_location": "Da click en un asset con coordenadas GPS para usar su ubicacion, o selecciona una ubicacion directamente en el mapa", - "geolocation_instruction_no_date": "Seleccione una fecha para administrar los datos de ubicación de las fotos y los videos de ese día", - "geolocation_instruction_no_photos": "No se encontraron fotos ni vídeos para esta fecha. Seleccione otra fecha para mostrarlos", "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", @@ -1417,10 +1412,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", @@ -1518,7 +1515,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. Toque dos veces el ícono del avatar del usuario para salir.", + "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}", @@ -1557,6 +1554,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", @@ -1603,8 +1601,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", @@ -1612,8 +1610,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", @@ -1623,7 +1621,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", @@ -1642,6 +1640,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", @@ -1735,7 +1734,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", + "selected_gps_coordinates": "Coordenadas GPS seleccionadas", "send_message": "Enviar mensaje", "send_welcome_email": "Enviar correo de bienvenida", "server_endpoint": "Punto final del servidor", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el archivo", "show_album_options": "Mostrar opciones del álbum", "show_albums": "Mostrar álbumes", - "show_all_assets": "Mostrar todos los assets", "show_all_people": "Mostrar todas las personas", "show_and_hide_people": "Mostrar y ocultar personas", - "show_assets_without_location": "Mostrar assets sin ubicación", "show_file_location": "Mostrar carpeta del archivo", "show_gallery": "Ver galería", "show_hidden_people": "Mostrar personas ocultas", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Uso biométrico", "use_current_connection": "Usar conexión actual", "use_custom_date_range": "Usa un intervalo de fechas personalizado", - "use_this_location": "Click para usar ubicación", "user": "Usuario", "user_has_been_deleted": "Este usuario ha sido eliminado.", "user_id": "Id. de usuario", @@ -2077,6 +2076,7 @@ "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", diff --git a/i18n/et.json b/i18n/et.json index d5573f6600..33a003bd54 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "See funktsionaalsus laadib töötamiseks Google'st väliseid ressursse.", "general": "Üldine", - "geolocation_instruction_all_have_location": "Kõigil selle kuupäevaga üksustel on juba asukoht. Proovi kuvada kõiki üksuseid või vali teine kuupäev", "geolocation_instruction_location": "Klõpsa GPS-koordinaatidega üksusel, et kasutada selle asukohta, või vali asukoht otse kaardilt", - "geolocation_instruction_no_date": "Vali kuupäev, et kõigi selle kuupäevaga fotode ja videote asukohti hallata", - "geolocation_instruction_no_photos": "Selle kuupäevaga fotosid ja videosid ei leitud. Vali mõni muu kuupäev", "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,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 topeltpuuduta avatari ikooni.", + "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", @@ -1642,6 +1639,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", @@ -1735,7 +1733,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", + "selected_gps_coordinates": "Valitud GPS-koordinaadid", "send_message": "Saada sõnum", "send_welcome_email": "Saada tervituskiri", "server_endpoint": "Serveri lõpp-punkt", @@ -1846,10 +1844,8 @@ "shift_to_permanent_delete": "vajuta ⇧, et üksus jäädavalt kustutada", "show_album_options": "Näita albumi valikuid", "show_albums": "Näita albumeid", - "show_all_assets": "Kuva kõik üksused", "show_all_people": "Näita kõiki isikuid", "show_and_hide_people": "Näita ja peida isikuid", - "show_assets_without_location": "Kuva ilma asukohata üksused", "show_file_location": "Näita faili asukohta", "show_gallery": "Näita galeriid", "show_hidden_people": "Kuva peidetud inimesed", @@ -1920,6 +1916,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", @@ -1957,7 +1955,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", @@ -1977,6 +1977,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", @@ -2032,7 +2033,6 @@ "use_biometric": "Kasuta biomeetriat", "use_current_connection": "kasuta praegust ühendust", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", - "use_this_location": "Klõpsa asukoha kasutamiseks", "user": "Kasutaja", "user_has_been_deleted": "See kasutaja on kustutatud.", "user_id": "Kasutaja ID", @@ -2075,6 +2075,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", 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/fi.json b/i18n/fi.json index 9f6f2bc2a9..2451b30a4b 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -396,6 +396,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", @@ -461,6 +463,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", @@ -594,8 +597,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", @@ -866,7 +867,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", @@ -1073,6 +1074,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", @@ -1633,8 +1635,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", diff --git a/i18n/fr.json b/i18n/fr.json index 19fa85fe31..29f9025516 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -389,7 +389,7 @@ "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.", @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Diffusion Google Cast", "gcast_enabled_description": "Cette fonctionnalité charge des ressources externes depuis Google pour fonctionner.", "general": "Général", - "geolocation_instruction_all_have_location": "Tous les médias pour cette date ont déjà des données de localisation. Essayez d'afficher tous les médias ou sélectionnez une date différente", "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", - "geolocation_instruction_no_date": "Sélectionnez une date pour gérer les données de localisation pour les photos et vidéos de ce jour", - "geolocation_instruction_no_photos": "Aucune photo ou vidéo trouvée pour cette date. Sélectionnez une date différente pour en afficher", "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,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 double sur l'image de l'utilisateur pour quitter.", + "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}", @@ -1557,6 +1554,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", @@ -1642,6 +1640,7 @@ "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", @@ -1735,7 +1734,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", + "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", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "appuyez sur ⇧ pour supprimer définitivement le média", "show_album_options": "Afficher les options de l'album", "show_albums": "Montrer les albums", - "show_all_assets": "Montrer tous les médias", "show_all_people": "Montrer toutes les personnes", "show_and_hide_people": "Afficher / Masquer les personnes", - "show_assets_without_location": "Montrer les médias sans localisation", "show_file_location": "Afficher l'emplacement du fichier", "show_gallery": "Afficher la galerie", "show_hidden_people": "Afficher les personnes masquées", @@ -1920,6 +1917,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", @@ -1965,7 +1964,7 @@ "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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Utiliser l'authentification biométrique", "use_current_connection": "Utiliser le réseau actuel", "use_custom_date_range": "Utilisez une plage de date personnalisée à la place", - "use_this_location": "Cliquez pour utiliser la localisation", "user": "Utilisateur", "user_has_been_deleted": "Cet utilisateur à été supprimé.", "user_id": "ID Utilisateur", @@ -2077,6 +2076,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", diff --git a/i18n/gl.json b/i18n/gl.json index ee1949250f..6851b8e7c2 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,6 +38,7 @@ "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", diff --git a/i18n/he.json b/i18n/he.json index 1030be626e..d8529413a0 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -396,6 +396,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": "מחק או שחזר תמונה במכשיר זה באופן אוטומטי כאשר פעולה זו נעשית בדפדפן", @@ -461,6 +463,7 @@ "app_bar_signout_dialog_title": "התנתק", "app_settings": "הגדרות יישום", "appears_in": "מופיע ב", + "apply_count": "החל ({count, number})", "archive": "ארכיון", "archive_action_prompt": "{count} נוספו לארכיון", "archive_or_unarchive_photo": "העבר תמונה לארכיון או הוצא אותה משם", @@ -594,8 +597,6 @@ "backup_setting_subtitle": "ניהול הגדרות העלאת רקע וחזית", "backup_settings_subtitle": "נהל הגדרות העלאה", "backward": "אחורה", - "beta_sync": "סטטוס סנכרון (בטא)", - "beta_sync_subtitle": "נהל את מערכת הסנכרון החדשה", "biometric_auth_enabled": "אימות ביומטרי הופעל", "biometric_locked_out": "גישה לאימות הביומטרי נחסמה", "biometric_no_options": "אין אפשרויות זמינות עבור אימות ביומטרי", @@ -842,7 +843,7 @@ "duration": "משך זמן", "edit": "ערוך", "edit_album": "ערוך אלבום", - "edit_avatar": "ערוך תמונת פרופיל", + "edit_avatar": "ערוך יצגן", "edit_birthday": "עריכת יום הולדת", "edit_date": "ערוך תאריך", "edit_date_and_time": "ערוך תאריך ושעה", @@ -1073,12 +1074,15 @@ "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": "קבץ לפי מדינה", @@ -1089,7 +1093,7 @@ "haptic_feedback_switch": "אפשר משוב ברטט", "haptic_feedback_title": "משוב ברטט", "has_quota": "יש מכסה", - "hash_asset": "גיבוב תמונה", + "hash_asset": "גבב פריט", "hashed_assets": "תמונות מגובבות", "hashing": "מגבב", "header_settings_add_header_tip": "הוסף כותרת", @@ -1125,7 +1129,7 @@ "hour": "שעה", "hours": "שעות", "id": "מזהה", - "idle": "ממתין", + "idle": "במצב סרק", "ignore_icloud_photos": "התעלם מתמונות iCloud", "ignore_icloud_photos_description": "תמונות שמאוחסנות ב-iCloud לא יועלו לשרת", "image": "תמונה", @@ -1262,6 +1266,7 @@ "main_branch_warning": "הגרסה המותקנת היא גרסת פיתוח; אנחנו ממליצים בחום להשתמש בגרסה יציבה!", "main_menu": "תפריט ראשי", "make": "תוצרת", + "manage_geolocation": "נהל מיקום", "manage_shared_links": "ניהול קישורים משותפים", "manage_sharing_with_partners": "ניהול שיתוף עם שותפים", "manage_the_app_settings": "ניהול הגדרות האפליקציה", @@ -1407,6 +1412,8 @@ "open_the_search_filters": "פתח את מסנני החיפוש", "options": "אפשרויות", "or": "או", + "organize_into_albums": "ארגן בתוך אלבומים", + "organize_into_albums_description": "שים תמונות קיימות בתוך אלבומים באמצעות הגדרות הסנכרון הנוכחיות", "organize_your_library": "ארגן את הספרייה שלך", "original": "מקורי", "other": "אחר", @@ -1466,9 +1473,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_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": "נראה ששיתפת את התמונות שלך עם כל המשתמשים או שאין לך אף משתמש לשתף איתו.", @@ -1508,6 +1515,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,13 +1554,16 @@ "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": "קרא את יומן השינויים", + "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 {# תמונות הוקצו}} מחדש לאדם חדש", @@ -1629,6 +1640,7 @@ "restore_user": "שחזר משתמש", "restored_asset": "התמונה שוחזרה", "resume": "המשך", + "resume_paused_jobs": "המשך {count, plural, one {עבודה # שהופסקה} other {# עבודות שהופסקו}}", "retry_upload": "נסה שוב להעלות", "review_duplicates": "בדוק כפילויות", "review_large_files": "צפייה בקבצים גדולים", @@ -1709,7 +1721,7 @@ "select_all": "בחר הכל", "select_all_duplicates": "בחר את כל הכפילויות", "select_all_in": "בחר הכול בתוך {group}", - "select_avatar_color": "בחר צבע תמונת פרופיל", + "select_avatar_color": "בחר צבע יצגן", "select_face": "בחר פנים", "select_featured_photo": "בחר תמונה מייצגת", "select_from_computer": "בחר מהמחשב", @@ -1722,6 +1734,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": "נקודת קצה שרת", @@ -1894,7 +1907,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 שלך נארזה על ידי צד שלישי. בעיות שאתה חווה עשויות להיגרם על ידי חבילה זו, אז בבקשה תעלה בעיות איתם ראשית כל באמצעות הקישורים למטה.", @@ -1903,7 +1916,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": "תיוג תמונות", @@ -1963,6 +1978,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", @@ -1993,6 +2009,7 @@ "unstacked_assets_count": "{count, plural, one {תמונה # הוסרה} other {# תמונות הוסרו}} מהערימה", "untagged": "לא מתיוגים", "up_next": "הבא בתור", + "update_location_action_prompt": "עדכן את המיקום של {count} פריטים שנבחרו עם:", "updated_at": "עודכן", "updated_password": "סיסמה עודכנה", "upload": "העלאה", @@ -2059,6 +2076,7 @@ "view_next_asset": "הצג את התמונה הבאה", "view_previous_asset": "הצג את התמונה הקודמת", "view_qr_code": "הצג ברקוד", + "view_similar_photos": "הצג תמונות דומות", "view_stack": "הצג ערימה", "view_user": "הצג משתמש", "viewer_remove_from_stack": "הסר מערימה", diff --git a/i18n/hi.json b/i18n/hi.json index 9662eb1203..90795e7330 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -585,8 +585,6 @@ "backup_setting_subtitle": "पृष्ठभूमि और अग्रभूमि अपलोड सेटिंग प्रबंधित करें", "backup_settings_subtitle": "अपलोड सेटिंग्स संभालें", "backward": "पिछला", - "beta_sync": "बीटा सिंक स्थिति", - "beta_sync_subtitle": "नए सिंक सिस्टम का प्रबंधन करें", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण सक्षम", "biometric_locked_out": "आप बायोमेट्रिक प्रमाणीकरण से बाहर हैं", "biometric_no_options": "कोई बायोमेट्रिक विकल्प उपलब्ध नहीं है", diff --git a/i18n/hr.json b/i18n/hr.json index 036078f23a..329ff1438d 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -594,8 +594,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..598910789d 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -594,8 +594,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", diff --git a/i18n/id.json b/i18n/id.json index f8de73b3be..abe57386ac 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", @@ -393,6 +396,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", @@ -458,6 +463,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", @@ -497,7 +503,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,6 +522,7 @@ "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", @@ -588,8 +597,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", @@ -1056,6 +1063,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 +1074,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 +1188,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 +1207,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", @@ -1253,6 +1266,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", @@ -1398,6 +1412,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 +1473,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.", @@ -1499,6 +1515,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. Ketuk dua kali 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 +1554,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 +1562,8 @@ "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", "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", @@ -1713,6 +1733,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", @@ -1856,6 +1877,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", @@ -1931,7 +1953,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", @@ -1981,6 +2005,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 +2072,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", diff --git a/i18n/it.json b/i18n/it.json index 568893331d..eca0ebf644 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -392,10 +392,12 @@ "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", @@ -461,6 +463,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", @@ -594,8 +597,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", @@ -1073,12 +1074,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", @@ -1181,7 +1185,7 @@ "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", @@ -1262,6 +1266,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", @@ -1407,6 +1412,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", @@ -1508,6 +1515,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}", @@ -1546,6 +1554,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", @@ -1553,6 +1562,8 @@ "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", "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", @@ -1629,6 +1640,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", @@ -1722,6 +1734,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", @@ -1993,6 +2006,7 @@ "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", @@ -2059,6 +2073,7 @@ "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", diff --git a/i18n/ja.json b/i18n/ja.json index 694f46419c..29e1a6a760 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -392,7 +392,7 @@ "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": "プロキシヘッダ", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "アップロードに関する設定", "backup_settings_subtitle": "アップロード設定を管理", "backward": "新しい方へ", - "beta_sync": "同期の状態", - "beta_sync_subtitle": "同期の仕組みを管理", "biometric_auth_enabled": "生体認証を有効化しました", "biometric_locked_out": "生体認証により、アクセスできません", "biometric_no_options": "生体認証を利用できません", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "この機能は動作のためにGoogleのリソースを読み込みます。", "general": "一般", - "geolocation_instruction_all_have_location": "この日付のすべての項目に位置情報がすでについています。すべての項目を表示を試みるか別の日付を選択してください", "geolocation_instruction_location": "位置情報付きの項目をクリックして、その位置情報を利用します。あるいは、地図上の地点を直接選ぶことも可能です", - "geolocation_instruction_no_date": "日付を選択して、その日の写真や動画の位置情報を管理しましょう", - "geolocation_instruction_no_photos": "この日付に写真や動画が無いようです。別の日付を選択してみてください", "get_help": "助けを求める", "get_wifiname_error": "Wi-Fiの名前(SSID)が入手できませんでした。Wi-Fiに繋がってるのと必要な権限を許可したか確認してください", "getting_started": "はじめる", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "検索フィルタを開く", "options": "オプション", "or": "または", + "organize_into_albums": "アルバムに追加して整理する", + "organize_into_albums_description": "既存の写真を、現在の同期設定に基づきアルバムに追加する", "organize_your_library": "ライブラリを整理", "original": "オリジナル", "other": "その他", @@ -1476,9 +1473,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": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。", @@ -1518,7 +1515,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_readonly_mode": "読み取り専用モードが有効です。ユーザーのアイコンを長押しして読み取り専用モードを解除してください。", "profile_drawer_server_out_of_date_major": "サーバーが更新されてません。最新のバージョンに更新してください", "profile_drawer_server_out_of_date_minor": "サーバーが更新されてません。最新のバージョンに更新してください", "profile_image_of_user": "{user} のプロフィール画像", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "サポーターの状態", "purchase_server_title": "サーバー", "purchase_settings_server_activated": "サーバーのプロダクトキーは管理者に管理されています", + "query_asset_id": "順番待ちの項目ID", "queue_status": "順番待ち中 {count}/{total}", "rating": "星での評価", "rating_clear": "評価を取り消す", @@ -1642,6 +1640,7 @@ "restore_user": "ユーザーを復元", "restored_asset": "項目を復元しました", "resume": "再開", + "resume_paused_jobs": "再開: {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "アップロードを再試行", "review_duplicates": "重複を調査", "review_large_files": "サイズの大きなファイルを見る", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "⇧を押してアセットを完全に削除", "show_album_options": "アルバム設定を表示", "show_albums": "アルバムを表示", - "show_all_assets": "すべての項目を表示", "show_all_people": "全ての人物を表示", "show_and_hide_people": "人物を表示/非表示", - "show_assets_without_location": "位置情報無しの項目を表示", "show_file_location": "ファイルの場所を表示", "show_gallery": "ギャラリーを表示", "show_hidden_people": "非表示の人物を表示", @@ -1920,6 +1917,8 @@ "sync_albums_manual_subtitle": "アップロード済みの全ての写真や動画を選択されたバックアップアルバムに同期する", "sync_local": "ローカルを同期", "sync_remote": "リモートを同期", + "sync_status": "同期の状態", + "sync_status_subtitle": "同期システムを確認・管理", "sync_upload_album_setting_subtitle": "サーバー上のアルバムの内容を端末上のアルバムと同期します (サーバーにアルバムが無い場合自動で作成されます。また、アップロードされていない写真や動画は同期されません)", "tag": "タグ付けする", "tag_assets": "アセットにタグ付けする", @@ -1979,6 +1978,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コードをセットアップできませんでした", @@ -2028,13 +2028,12 @@ "upload_success": "アップロード成功、新しくアップロードされたアセットを見るにはページを更新してください。", "upload_to_immich": "Immichにアップロード ({count})", "uploading": "アップロード中", - "uploading_media": "メディアをアップロード", + "uploading_media": "メディアをアップロード中", "url": "URL", "usage": "使用容量", "use_biometric": "生体認証をご利用ください", "use_current_connection": "現在の接続情報を使用", "use_custom_date_range": "代わりにカスタム日付範囲を使用", - "use_this_location": "クリックして位置情報を使う", "user": "ユーザー", "user_has_been_deleted": "このユーザーは削除されました", "user_id": "ユーザーID", @@ -2077,6 +2076,7 @@ "view_next_asset": "次のアセットを見る", "view_previous_asset": "前のアセットを見る", "view_qr_code": "QRコードを見る", + "view_similar_photos": "類似する写真を見る", "view_stack": "ビュースタック", "view_user": "ユーザーを見る", "viewer_remove_from_stack": "スタックから外す", diff --git a/i18n/ko.json b/i18n/ko.json index ae269037c2..09d215c2a3 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -596,8 +596,6 @@ "backup_setting_subtitle": "백그라운드 및 포그라운드 백업 설정을 관리합니다.", "backup_settings_subtitle": "업로드 설정을 관리합니다.", "backward": "뒤로", - "beta_sync": "동기화 (베타) 상태", - "beta_sync_subtitle": "새 동기화 시스템의 설정을 관리합니다.", "biometric_auth_enabled": "생체 인증이 활성화되었습니다.", "biometric_locked_out": "생체 인증이 일시적으로 비활성화되었습니다.", "biometric_no_options": "사용 가능한 생체 인증 옵션 없음", @@ -1081,6 +1079,8 @@ "go_back": "뒤로", "go_to_folder": "폴더로 이동", "go_to_search": "검색으로 이동", + "gps": "GPS", + "gps_missing": "GPS 없음", "grant_permission": "권한 부여", "group_albums_by": "다음으로 앨범 그룹화...", "group_country": "국가별로 그룹화", @@ -1264,6 +1264,7 @@ "main_branch_warning": "개발 버전을 사용 중입니다. 정식 릴리스 버전 사용을 권장합니다!", "main_menu": "메인 메뉴", "make": "제조사", + "manage_geolocation": "위치 정보 관리", "manage_shared_links": "공유 링크 관리", "manage_sharing_with_partners": "공유할 파트너를 초대하거나 제거합니다.", "manage_the_app_settings": "앱 동작 및 표시 환경을 사용자 정의합니다.", diff --git a/i18n/lt.json b/i18n/lt.json index a35ddfa775..4392fdb944 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.", @@ -165,7 +172,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 +189,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", @@ -303,29 +312,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 +386,36 @@ "admin_email": "Administratoriaus el. paštas", "admin_password": "Administratoriaus slaptažodis", "administration": "Administravimas", + "advanced": "Sudėtingesnis", + "advanced_settings_beta_timeline_subtitle": "Išbandykite naujos programos patirtį", + "advanced_settings_beta_timeline_title": "Beta laiko juosta", + "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 +423,27 @@ "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_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 +452,154 @@ "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": "Maišoma…", + "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_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_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ą", + "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_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 +608,104 @@ "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", + "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": "Atlikta", "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 +716,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 +725,115 @@ "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", + "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 +841,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 +856,79 @@ "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_loading_image": "Klaida įkeliant vaizdą", + "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 +936,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 +953,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 +962,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 +1001,104 @@ "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_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": "Maišymo elementas", + "hashed_assets": "Sumaišyti elementai", + "hashing": "Maišoma", + "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,7 +1106,17 @@ "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 kad negalima priskirti mėgstamiausių vietinių 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", "hour": "Valanda", @@ -707,7 +1161,7 @@ "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", + "location_permission_content": "Norint naudoti automatinio persijungimo opciją, Immich reikia tikslios vietovės leidimo, kad galėtų nuskaityti Wi-Fi tinklo pavadinimą", "locked_folder": "Užrakintas aplankas", "log_out": "Atsijungti", "log_out_all_devices": "Atsijungti iš visų įrenginių", @@ -727,7 +1181,10 @@ "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_settings": "Žemėlapio nustatymai", + "map_settings_date_range_option_days": "Pastarąsias {days} dienas", + "map_settings_date_range_option_years": "Pastaruosius {years} metus", "map_settings_include_show_archived": "Įtraukti archyvuotus", "matches": "Atitikmenys", "media_type": "Laikmenos tipas", @@ -785,6 +1242,7 @@ "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", "oldest_first": "Seniausias pirmas", @@ -805,6 +1263,7 @@ "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_page_stop_sharing_content": "{partner} daugiau nebegalės pasiekti jūsų nuotraukų.", "partners": "Partneriai", "password": "Slaptažodis", "password_does_not_match": "Slaptažodis nesutampa", @@ -859,7 +1318,7 @@ "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 +1457,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 +1469,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 +1571,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 +1590,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 +1628,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 5add5fc0b8..b0c77998b3 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}", @@ -45,13 +45,14 @@ "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.", + "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?", @@ -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", @@ -187,6 +193,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,10 +212,13 @@ "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", @@ -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", @@ -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})", @@ -337,6 +349,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", @@ -379,8 +392,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", @@ -406,6 +417,7 @@ "cache_settings_title": "Kešdarbes iestatījumi", "camera": "Fotokamera", "cancel": "Atcelt", + "canceled": "Atcelts", "canceling": "Atceļ", "cannot_merge_people": "Nevar apvienot personas", "cast": "Pārraidīt", @@ -424,12 +436,19 @@ "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", + "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ā", @@ -441,6 +460,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", @@ -463,6 +483,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", @@ -471,6 +492,7 @@ "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", @@ -524,21 +546,27 @@ "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", @@ -572,6 +600,7 @@ "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_saving_image": "Kļūda: {error}", "errors": { "cant_get_faces": "Nevar iegūt sejas", @@ -625,10 +654,13 @@ "export_database_description": "Eksportēt SQLite datubāzi", "extension": "Paplašinājums", "external": "Ārējs", + "external_network": "Ārējs tīkls", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "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", @@ -654,6 +686,8 @@ "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", @@ -744,6 +778,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", @@ -751,6 +786,8 @@ "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ē", "local_network": "Lokālais tīkls", @@ -789,6 +826,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", @@ -856,6 +894,7 @@ "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", + "networking_subtitle": "Pārvaldīt servera galapunktu iestatījumus", "never": "nekad", "new_album": "Jauns albums", "new_api_key": "Jauna API atslēga", @@ -881,6 +920,7 @@ "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.", @@ -907,6 +947,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", @@ -1115,12 +1157,17 @@ "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 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ē", @@ -1147,6 +1194,7 @@ "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.", "settings": "Iestatījumi", "settings_require_restart": "Lūdzu, restartējiet Immich, lai lietotu šo iestatījumu", "setup_pin_code": "Uzstādīt PIN kodu", @@ -1255,6 +1303,8 @@ "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_tiles_per_row_title": "Failu skaits rindā ({count})", @@ -1295,6 +1345,7 @@ "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", @@ -1325,6 +1376,7 @@ "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", @@ -1338,6 +1390,7 @@ "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", @@ -1358,6 +1411,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/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..33a3dc5c93 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": "संग्रहित केले", @@ -572,8 +574,6 @@ "backup_setting_subtitle": "बॅकग्राउंड आणि फोरग्राउंड अपलोड सेटिंग्ज व्यवस्थापित करा", "backup_settings_subtitle": "अपलोड सेटिंग्ज व्यवस्थापित करा", "backward": "मागासलेले", - "beta_sync": "बीटा सिंक स्थिती", - "beta_sync_subtitle": "नवीन सिंक प्रणाली व्यवस्थापित करा", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण चालू आहे", "biometric_locked_out": "आपण बायोमेट्रिक प्रमाणीकरणापासून लॉक आहात", "biometric_no_options": "कोणतेही बायोमेट्रिक पर्याय उपलब्ध नाहीत", @@ -1056,6 +1056,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/nb_NO.json b/i18n/nb_NO.json index 37d19afaec..37c5c3011e 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funksjonen laster eksterne ressurser fra Google for å fungere.", "general": "Generelt", - "geolocation_instruction_all_have_location": "Alle objekter for denne datoen har allerede lokasjonsdata. Prøv å vise alle objekter eller velg en annen dato", "geolocation_instruction_location": "Klikk på et objekt med GPS-koordinater for å bruke posisjonen, eller velg en posisjon direkte fra kartet", - "geolocation_instruction_no_date": "Velg en dato for å administrere posisjonsdata for bilder og videoer fra den dagen", - "geolocation_instruction_no_photos": "Ingen bilder eller videoer funnet for denne datoen. Velg en annen dato for å vise dem", "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,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. Dobbelttrykk på brukerens avatarikon for å avslutte.", + "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}", @@ -1557,6 +1554,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", @@ -1642,6 +1640,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", @@ -1735,7 +1734,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", + "selected_gps_coordinates": "Valgte GPS-koordinater", "send_message": "Send melding", "send_welcome_email": "Send velkomstmelding", "server_endpoint": "Server endepunkt", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "trykk ⇧ for å slette eiendeler permanent", "show_album_options": "Vis albumalternativer", "show_albums": "Vis album", - "show_all_assets": "Via alle objekter", "show_all_people": "Vis alle mennesker", "show_and_hide_people": "Vis og skjul personer", - "show_assets_without_location": "Vis objekter uten lokasjon", "show_file_location": "Vis filplassering", "show_gallery": "Vis galleri", "show_hidden_people": "Vis skjulte personer", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Bruk biometri", "use_current_connection": "bruk nåværende tilkobling", "use_custom_date_range": "Bruk egendefinert datoperiode i stedet", - "use_this_location": "Trykk for å bruke lokasjon", "user": "Bruker", "user_has_been_deleted": "Denne brukeren har blitt slettet.", "user_id": "Bruker ID", @@ -2077,6 +2076,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", diff --git a/i18n/nl.json b/i18n/nl.json index f876640740..cfd8ffca98 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -293,7 +293,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 +312,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 +325,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 +341,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.", @@ -396,6 +396,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", @@ -461,6 +463,7 @@ "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} item(s) toegevoegd aan het archief", "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen", @@ -594,8 +597,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", @@ -707,7 +708,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": "Verwijderen uit Immich", + "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", @@ -834,7 +835,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": "Downloading asset {filename}", "downloading_media": "Media aan het downloaden", "drop_files_to_upload": "Zet bestanden ergens neer om ze te uploaden", "duplicates": "Duplicaten", @@ -1073,12 +1074,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", @@ -1262,6 +1266,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", @@ -1407,6 +1412,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", @@ -1503,18 +1510,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 +1554,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 +1562,8 @@ "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", "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", @@ -1629,6 +1640,7 @@ "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 +1694,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 +1734,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", @@ -1904,6 +1917,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,7 +1956,9 @@ "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", @@ -1961,6 +1978,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", @@ -1991,6 +2009,7 @@ "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", @@ -2057,6 +2076,7 @@ "view_next_asset": "Bekijk volgende item", "view_previous_asset": "Bekijk vorige 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", diff --git a/i18n/pl.json b/i18n/pl.json index f68ab2f205..dd0ba8cd4d 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -44,7 +44,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ć", @@ -396,6 +396,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", @@ -441,7 +443,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 +461,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", @@ -501,7 +504,7 @@ "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} other {# zasobów}} do {albumTotal, plural, one {# albumu} other {# 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_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,9 +521,9 @@ "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", @@ -528,22 +531,22 @@ "back_close_deselect": "Wróć, zamknij lub odznacz", "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", + "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_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 +563,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 +593,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 +638,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ę", @@ -690,7 +691,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 +708,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", @@ -773,7 +774,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 +786,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 +837,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", @@ -897,7 +898,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 +923,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.", @@ -1073,12 +1074,15 @@ "gcast_enabled": "Google Cast", "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 +1151,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 +1175,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 +1190,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", @@ -1262,6 +1266,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", @@ -1407,6 +1412,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 +1424,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 +1459,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", @@ -1504,12 +1511,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 +1554,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 +1562,8 @@ "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", "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", @@ -1578,7 +1589,7 @@ "remote": "Zdalny", "remote_assets": "Zasoby zdalne", "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", @@ -1629,6 +1640,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", @@ -1700,7 +1712,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 +1732,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", @@ -1874,9 +1887,9 @@ "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", @@ -1885,12 +1898,12 @@ "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 +1914,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,7 +1952,7 @@ "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ę", @@ -1946,12 +1961,12 @@ "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.", @@ -1963,10 +1978,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", @@ -1988,11 +2004,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", @@ -2059,11 +2076,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", diff --git a/i18n/pt.json b/i18n/pt.json index 6ede1a04c4..97673bb7a5 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -28,6 +28,9 @@ "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_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", @@ -355,6 +358,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", @@ -390,6 +396,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": "Activa o modo somente leitura, onde as fotos podem ser visualizadas. Recursos como selecionar várias imagens, partilhar, transmitir e excluir ficam deactivados. Activar/Desactivar 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", @@ -455,6 +463,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", @@ -494,7 +503,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,6 +522,7 @@ "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", @@ -585,8 +597,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", @@ -749,6 +759,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 +844,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", @@ -909,6 +922,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 +931,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", @@ -1048,21 +1063,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 +1127,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 +1188,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 +1207,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", @@ -1243,6 +1266,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 +1275,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", @@ -1295,6 +1319,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 +1339,9 @@ "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_updated": "Requisitos de rede alterados, redefinindo fila de backup", "networking_settings": "Conexões", "networking_subtitle": "Gerencie a conexão do servidor", "never": "Nunca", @@ -1365,6 +1393,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 +1412,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 +1473,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.", @@ -1481,6 +1515,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 +1554,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 +1562,8 @@ "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 desactivado", + "readonly_mode_enabled": "Modo somente leitura activado", "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 +1625,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 +1640,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 +1734,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 +1802,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 +1828,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.}}", @@ -1832,6 +1878,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", @@ -1870,6 +1917,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 +1956,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 +1978,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 +2009,7 @@ "unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}", "untagged": "Marcador removido", "up_next": "A seguir", + "update_location_action_prompt": "Actualize a localização de {count} activos seleccionados com:", "updated_at": "Atualizado a", "updated_password": "Palavra-passe atualizada", "upload": "Carregar", @@ -2023,6 +2076,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 0ca13f3295..5115f9ff6a 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade carrega recursos externos do Google para funcionar.", "general": "Geral", - "geolocation_instruction_all_have_location": "Todos arquivos nesta data já contem dados de localização. Tente exibir todos os arquivos ou selecione uma data diferente", - "geolocation_instruction_location": "Selecione o arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa", - "geolocation_instruction_no_date": "Selecione uma data para gerenciar os dados de localização das fotos e vídeos daquele dia", - "geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado nesta data. Selecione uma data diferente para ser exibida", + "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,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 visualização ativado. Toque duas vezes na foto do usuário para sair deste modo.", + "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}", @@ -1557,6 +1554,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", @@ -1642,6 +1640,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", @@ -1735,7 +1734,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": "selecione as coordenadas de GPS", + "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", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "pressione ⇧ para excluir permanentemente o arquivo", "show_album_options": "Exibir opções do álbum", "show_albums": "Exibir álbuns", - "show_all_assets": "Ver todos arquivos", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", - "show_assets_without_location": "Ver arquivos sem localização", "show_file_location": "Exibir local do arquivo", "show_gallery": "Exibir galeria", "show_hidden_people": "Exibir pessoas ocultadas", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Usar biometria", "use_current_connection": "usar conexão atual", "use_custom_date_range": "Usar intervalo de datas personalizado", - "use_this_location": "Clique para marcar o local", "user": "Usuário", "user_has_been_deleted": "Este usuário foi excluído.", "user_id": "ID do usuário", @@ -2077,6 +2076,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", diff --git a/i18n/ro.json b/i18n/ro.json index 8ca0b87ef0..fc2ca444c6 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -28,6 +28,9 @@ "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_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ă", @@ -181,6 +184,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", @@ -354,6 +358,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", @@ -389,6 +396,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", @@ -454,6 +463,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", @@ -493,7 +503,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", @@ -583,8 +595,6 @@ "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", diff --git a/i18n/ru.json b/i18n/ru.json index 4495196755..287f0288bd 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -35,7 +35,7 @@ "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": "Администратор", @@ -396,7 +396,7 @@ "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_subtitle": "Включает режим, в котором можно только просматривать объекты. Функции выбора нескольких объектов, публикации, трансляции и удаления будут недоступны. Включить/отключить этот режим можно удерживая значок аватара пользователя на главном экране.", "advanced_settings_readonly_mode_title": "Режим «только просмотр»", "advanced_settings_self_signed_ssl_subtitle": "Пропускать проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.", "advanced_settings_self_signed_ssl_title": "Разрешить самоподписанные SSL-сертификаты", @@ -409,7 +409,7 @@ "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": "Если альбом был общим, другие пользователи больше не смогут получить к нему доступ.", @@ -426,7 +426,7 @@ "album_search_not_found": "Не найдено альбомов по вашему запросу", "album_share_no_users": "Нет доступных пользователей, с которыми можно поделиться альбомом.", "album_updated": "Альбом обновлён", - "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых ресурсов в общий альбом", + "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых объектов в общий альбом", "album_user_left": "Вы покинули {album}", "album_user_removed": "Пользователь {user} удален", "album_viewer_appbar_delete_confirm": "Вы уверены, что хотите удалить альбом из своей учетной записи?", @@ -448,7 +448,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": "Разрешить добавление файлов", @@ -534,9 +534,9 @@ "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_all": "Все", "backup_background_service_backup_failed_message": "Не удалось выполнить резервное копирование. Повторная попытка…", @@ -544,9 +544,9 @@ "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": "Перейти в настройки", @@ -563,8 +563,8 @@ "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": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты при открытии приложения.", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Настройка активного и фонового резервного копирования", "backup_settings_subtitle": "Настройка загрузки объектов", "backward": "Назад", - "beta_sync": "Статус синхронизации", - "beta_sync_subtitle": "Управление новой системой синхронизации", "biometric_auth_enabled": "Биометрическая аутентификация включена", "biometric_locked_out": "Вам закрыт доступ к биометрической аутентификации", "biometric_no_options": "Биометрическая аутентификация недоступна", @@ -637,8 +635,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": "Изменить порядок отображения", @@ -770,7 +768,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 ключ?", @@ -827,7 +825,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": "Скачивание", @@ -1043,7 +1041,7 @@ "external": "Внешний", "external_libraries": "Внешние библиотеки", "external_network": "Внешняя сеть", - "external_network_sheet_info": "Когда устройство не подключено к выбранной Wi-Fi сети, приложение будет пытаться подключиться к серверу по адресам ниже, сверху вниз, до успешного подключения", + "external_network_sheet_info": "Когда устройство не подключено к указанной Wi-Fi сети, приложение будет пытаться подключиться к серверу по адресам ниже, сверху вниз до успешного подключения", "face_unassigned": "Не назначено", "failed": "Ошибка", "failed_to_authenticate": "Ошибка аутентификации", @@ -1070,16 +1068,13 @@ "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_all_have_location": "Все объекты в этом периоде уже содержат данные о местоположении. Включите отображение всех объектов или укажите другой период.", "geolocation_instruction_location": "Выберите объект с имеющимися координатами, чтобы использовать их, либо вручную укажите место на карте", - "geolocation_instruction_no_date": "Укажите дату для управления координатами мест съёмки за этот день", - "geolocation_instruction_no_photos": "Не найдено объектов в этом периоде. Укажите другую дату.", "get_help": "Получить помощь", "get_wifiname_error": "Не удалось получить имя Wi-Fi сети. Убедитесь, что вы подключены к сети и предоставили приложению необходимые разрешения", "getting_started": "Старт", @@ -1098,8 +1093,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": "Значение не может быть пустым", @@ -1117,14 +1112,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": "Невозможно переместить объекты партнёра в личную папку, пропуск", @@ -1159,8 +1154,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": "Информация", @@ -1221,10 +1216,10 @@ "loading": "Загрузка", "loading_search_results_failed": "Загрузка результатов поиска не удалась", "local": "На устройстве", - "local_asset_cast_failed": "Невозможно транслировать объект, который ещё не загружен на сервер", + "local_asset_cast_failed": "Невозможна трансляция объектов, которые ещё не загружены на сервер", "local_assets": "Объекты на устройстве", "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": "Выбрать на карте", @@ -1273,7 +1268,7 @@ "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 ключами для взаимодействия с другими программами", @@ -1372,7 +1367,7 @@ "no_duplicates_found": "Дубликатов не обнаружено.", "no_exif_info_available": "Нет доступной информации exif", "no_explore_results_message": "Загружайте больше фотографий, чтобы наслаждаться вашей коллекцией.", - "no_favorites_message": "Добавляйте в избранное, чтобы быстро найти свои лучшие фотографии и видео", + "no_favorites_message": "Добавляйте объекты в избранное, чтобы быстрее находить свои лучшие фото и видео", "no_libraries_message": "Создайте внешнюю библиотеку для просмотра в Immich сторонних фотографий и видео", "no_locked_photos_message": "Фото и видео, перемещенные в личную папку, скрыты и не отображаются при просмотре библиотеки.", "no_name": "Нет имени", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Открыть фильтры поиска", "options": "Опции", "or": "или", + "organize_into_albums": "Распределить по альбомам", + "organize_into_albums_description": "Добавить уже существующие объекты в альбомы, используя текущие настройки синхронизации", "organize_your_library": "Приведите в порядок свою библиотеку", "original": "оригинал", "other": "Другое", @@ -1425,18 +1422,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": "Пароль", @@ -1456,7 +1453,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": "Предупреждать перед безвозвратным удалением объектов", @@ -1518,7 +1515,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_readonly_mode": "Включён режим «только просмотр». Удерживайте значок аватара пользователя для отключения.", "profile_drawer_server_out_of_date_major": "Версия сервера устарела. Пожалуйста, обновите его.", "profile_drawer_server_out_of_date_minor": "Версия сервера устарела. Пожалуйста, обновите его.", "profile_image_of_user": "Изображение профиля {user}", @@ -1557,11 +1554,12 @@ "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": "Показывать рейтинг в панели информации", + "rating_description": "Система оценки объектов в панели информации", "reaction_options": "Опции реакций", "read_changelog": "Прочитать список изменений", "readonly_mode_disabled": "Режим «только просмотр» отключён", @@ -1642,6 +1640,7 @@ "restore_user": "Восстановить пользователя", "restored_asset": "Восстановленный объект", "resume": "Продолжить", + "resume_paused_jobs": "Возобновить выполнение {count, plural, one {# задачи} other {# задач}}", "retry_upload": "Повторить загрузку", "review_duplicates": "Разбор дубликатов", "review_large_files": "Обзор больших файлов", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "Не удалось создать альбом", "selected": "Выбрано", "selected_count": "{count, plural, one {Выбран # объект} many {Выбрано # объектов} other {Выбрано # объекта}}", - "selected_gps_coordinates": "выбранные координаты", + "selected_gps_coordinates": "Выбранные координаты", "send_message": "Отправить сообщение", "send_welcome_email": "Отправить приветственное письмо", "server_endpoint": "Адрес сервера", @@ -1797,7 +1796,7 @@ "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": "Скопировано в буфер обмена", @@ -1834,7 +1833,7 @@ "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": "Общие альбомы", @@ -1846,15 +1845,13 @@ "shift_to_permanent_delete": "нажмите ⇧ чтобы удалить объект навсегда", "show_album_options": "Показать параметры альбома", "show_albums": "Показать альбомы", - "show_all_assets": "Показать все объекты", "show_all_people": "Показать всех людей", "show_and_hide_people": "Показать и скрыть людей", - "show_assets_without_location": "Показать объекты без координат", "show_file_location": "Показать расположение файла", "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": "Показать или скрыть информацию", @@ -1868,7 +1865,7 @@ "show_supporter_badge_description": "Показать значок поддержки", "shuffle": "Перемешать", "sidebar": "Боковая панель", - "sidebar_display_description": "Показывать ссылку на представление в боковой панели", + "sidebar_display_description": "Отображать раздел на боковой панели", "sign_out": "Выход", "sign_up": "Зарегистрироваться", "size": "Размер", @@ -1900,9 +1897,9 @@ "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": "Квота хранилища", @@ -1918,9 +1915,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} создан", @@ -1934,7 +1933,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": "Добавить оттенок к фону.", @@ -1979,6 +1978,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-кода", @@ -2034,7 +2034,6 @@ "use_biometric": "Использовать биометрию", "use_current_connection": "Использовать текущее подключение", "use_custom_date_range": "Использовать пользовательский диапазон дат", - "use_this_location": "Выбрать это место", "user": "Пользователь", "user_has_been_deleted": "Этот пользователь был удалён.", "user_id": "ID пользователя", @@ -2073,10 +2072,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": "Убрать из группы", diff --git a/i18n/sk.json b/i18n/sk.json index 20e00836e1..3113ce788d 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "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_all_have_location": "Všetky položky pre tento dátum už majú údaje o polohe. Skúste zobraziť všetky položky alebo vyberte iný dátum", "geolocation_instruction_location": "Kliknite na položku s GPS súradnicami, aby ste použili jej polohu, alebo vyberte polohu priamo z mapy", - "geolocation_instruction_no_date": "Vyberte dátum, aby ste mohli spravovať údaje o polohe pre fotografie a videá z daného dňa", - "geolocation_instruction_no_photos": "Pre tento dátum neboli nájdené žiadne fotografie ani videá. Vyberte iný dátum, aby sa zobrazili", "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", @@ -1417,6 +1412,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é", @@ -1518,7 +1515,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ý. Dvojitým ťuknutím na ikonu obrázku používateľa režim opustíte.", + "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}", @@ -1557,6 +1554,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", @@ -1642,6 +1640,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", @@ -1735,7 +1734,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", + "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", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "stlačte ⇧ na trvalé vymazanie položky", "show_album_options": "Zobraziť možnosti albumu", "show_albums": "Zobraziť albumy", - "show_all_assets": "Zobraziť všetky položky", "show_all_people": "Zobraziť všetkých ľudí", "show_and_hide_people": "Zobraziť a skryť ľudí", - "show_assets_without_location": "Zobraziť položky bez polohy", "show_file_location": "Zobraziť umiestnenie súboru", "show_gallery": "Zobraziť galériu", "show_hidden_people": "Zobraziť skrytých ľudí", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Použiť biometrické údaje", "use_current_connection": "použiť aktuálne pripojenie", "use_custom_date_range": "Použiť radšej vlastný rozsah dátumov", - "use_this_location": "Kliknutím použite polohu", "user": "Používateľ", "user_has_been_deleted": "Tento používateľ bol vymazaný.", "user_id": "ID používateľa", @@ -2077,6 +2076,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", diff --git a/i18n/sl.json b/i18n/sl.json index 882d81c141..f67a6b08b7 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ta funkcija za delovanje nalaga zunanje vire iz Googla.", "general": "Splošno", - "geolocation_instruction_all_have_location": "Vsa sredstva za ta datum že imajo podatke o lokaciji. Poskusite prikazati vsa sredstva ali izberite drug datum", "geolocation_instruction_location": "Kliknite na sredstvo z GPS koordinatami, da uporabite njegovo lokacijo, ali pa izberite lokacijo neposredno na zemljevidu", - "geolocation_instruction_no_date": "Izberite datum za upravljanje podatkov o lokaciji za fotografije in videoposnetke s tega dne", - "geolocation_instruction_no_photos": "Za ta datum ni bilo najdenih fotografij ali videoposnetkov. Izberite drug datum, da jih prikažete", "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,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 dvakrat tapnite ikono uporabniškega avatarja.", + "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}", @@ -1557,6 +1554,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", @@ -1642,6 +1640,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", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "pritisni ⇧ za trajno brisanje sredstva", "show_album_options": "Prikaži možnosti albuma", "show_albums": "Prikaži albume", - "show_all_assets": "Prikaži vsa sredstva", "show_all_people": "Prikaži vse osebe", "show_and_hide_people": "Prikaži & skrij osebe", - "show_assets_without_location": "Prikaži sredstva brez lokacije", "show_file_location": "Pokaži lokacijo datoteke", "show_gallery": "Prikaži galerijo", "show_hidden_people": "Prikaži skrite osebe", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Uporabite biometrične podatke", "use_current_connection": "uporabi trenutno povezavo", "use_custom_date_range": "Namesto tega uporabite časovno obdobje po meri", - "use_this_location": "Kliknite za uporabo lokacije", "user": "Uporabnik", "user_has_been_deleted": "Ta uporabnik je bil izbrisan.", "user_id": "ID uporabnika", @@ -2077,6 +2076,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", 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 7d8a618475..2e1d394bdd 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -597,8 +597,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", @@ -1076,10 +1074,7 @@ "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_all_have_location": "Alla tillgångar för detta datum har redan platsdata. Försök att visa alla tillgångar eller välj ett annat datum", "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", - "geolocation_instruction_no_date": "Välj ett datum för att hantera platsdata för foton och videor från den dagen", - "geolocation_instruction_no_photos": "Inga foton eller videor hittades för detta datum. Välj ett annat datum för att visa dem", "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", @@ -1417,6 +1412,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", @@ -1518,7 +1515,7 @@ "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_readonly_mode": "Skrivskyddat läge aktiverat. Dubbeltryck på användaravatarikonen för att avsluta", + "profile_drawer_readonly_mode": "Skrivskyddat läge aktiverat. Håll in 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", @@ -1557,6 +1554,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", @@ -1642,6 +1640,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", @@ -1735,7 +1734,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", + "selected_gps_coordinates": "Valda GPS-koordinater", "send_message": "Skicka meddelande", "send_welcome_email": "Skicka välkomstmejl", "server_endpoint": "Server-endpoint", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "tryck på ⇧ för att permanent radera tillgången", "show_album_options": "Visa albumalternativ", "show_albums": "Visa album", - "show_all_assets": "Visa alla tillgångar", "show_all_people": "Visa alla personer", "show_and_hide_people": "Visa & göm personer", - "show_assets_without_location": "Visa tillgångar utan plats", "show_file_location": "Visa sökväg", "show_gallery": "Visa galleri", "show_hidden_people": "Visa gömda personer", @@ -1920,6 +1917,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", @@ -1979,6 +1978,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", @@ -2034,7 +2034,6 @@ "use_biometric": "Använd biometri", "use_current_connection": "Använd aktuell anslutning", "use_custom_date_range": "Använd anpassat datumintervall istället", - "use_this_location": "Klicka för att använda plats", "user": "Användare", "user_has_been_deleted": "Den här användaren har raderats.", "user_id": "Användar-ID", @@ -2077,6 +2076,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", diff --git a/i18n/ta.json b/i18n/ta.json index d2b20ed7c2..96bec1f2f0 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -167,6 +167,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": "முக இறக்குமதியை இயக்கவும்", @@ -175,6 +177,20 @@ "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": "குறிப்பு: முன்பு பதிவேற்றிய படங்களுக்கு சேமிப்பக லேபிளைப் பயன்படுத்த, இதை இயக்கவும்", @@ -200,11 +216,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": "சேமிப்பக லேபிள் உரிமைகோரல்", @@ -212,7 +231,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": "கடவுச்சொல் உள்நுழைவு அமைப்புகளை நிர்வகிக்கவும்", @@ -250,6 +271,7 @@ "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": "பதிவேற்ற புகைப்படங்களின் கோப்புறை அமைப்பு மற்றும் கோப்பு பெயரை நிர்வகிக்கவும்", @@ -293,6 +315,8 @@ "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_decoding": "வன்பொருள் டிகோடிங்", @@ -304,6 +328,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": "முன்னமைக்கப்பட்ட (-பிரசெட்)", @@ -312,6 +338,7 @@ "transcoding_reference_frames_description": "கொடுக்கப்பட்ட சட்டகத்தை சுருக்கும்போது குறிப்பிட வேண்டிய பிரேம்களின் எண்ணிக்கை. அதிக மதிப்புகள் சுருக்க செயல்திறனை மேம்படுத்துகின்றன, ஆனால் குறியாக்கத்தை மெதுவாக்குகின்றன. 0 இந்த மதிப்பை தானாக அமைக்கிறது.", "transcoding_required_description": "ஏற்றுக்கொள்ளப்பட்ட வடிவத்தில் இல்லாத வீடியோக்கள் மட்டுமே", "transcoding_settings": "வீடியோ டிரான்ச்கோடிங் அமைப்புகள்", + "transcoding_settings_description": "எந்த வீடியோக்களை டிரான்ஸ்கோட் செய்ய வேண்டும், அவற்றை எவ்வாறு செயலாக்க வேண்டும் என்பதை நிர்வகிக்கவும்", "transcoding_target_resolution": "இலக்கு தீர்மானம்", "transcoding_target_resolution_description": "அதிக தீர்மானங்கள் அதிக விவரங்களை பாதுகாக்க முடியும், ஆனால் குறியாக்க அதிக நேரம் எடுக்கும், பெரிய கோப்பு அளவுகளைக் கொண்டிருக்கலாம், மேலும் பயன்பாட்டு மறுமொழியைக் குறைக்கலாம்.", "transcoding_temporal_aq": "தம்போர்ல்", @@ -324,18 +351,23 @@ "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_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": "தயவுசெய்து தற்காலிக கடவுச்சொல்லை பயனருக்கு வழங்கவும், அவர்களின் அடுத்த உள்நுழைவில் கடவுச்சொல்லை மாற்ற வேண்டும் என்று அவர்களுக்குத் தெரிவிக்கவும்.", @@ -355,6 +387,18 @@ "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_title": "ரிமோட் படங்களுக்கு முன்னுரிமை கொடு", + "advanced_settings_proxy_headers_title": "ப்ராக்ஸி தலைப்புகள்", + "advanced_settings_readonly_mode_subtitle": "புகைப்படங்களை மட்டும் பார்க்கக்கூடிய படிக்க மட்டும் பயன்முறையை இயக்குகிறது, பல படங்களைத் தேர்ந்தெடுப்பது, பகிர்தல், அனுப்புதல், நீக்குதல் போன்ற அனைத்தும் முடக்கப்பட்டுள்ளன. பிரதான திரையில் இருந்து பயனர் அவதார் வழியாக படிக்க மட்டும் என்பதை இயக்கு/முடக்கு", + "advanced_settings_readonly_mode_title": "படிக்க மட்டுமேயான பயன்முறை", + "advanced_settings_self_signed_ssl_title": "சுய கையொப்பமிட்ட SSL சான்றிதழ்களை அனுமதி", + "advanced_settings_sync_remote_deletions_subtitle": "இணையத்தில் நடவடிக்கை எடுக்கப்படும்போது, இந்தச் சாதனத்தில் உள்ள ஒரு சொத்தை தானாகவே நீக்கவும் அல்லது மீட்டெடுக்கவும்", "age_months": "அகவை {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}}", "age_year_months": "அகவை 1 அகவை, {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}}", "age_years": "{ஆண்டுகள், பன்மை, பிற {வயது #}}", @@ -1269,12 +1313,21 @@ "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} {பாத்திரமாக அமைக்கவும்", @@ -1283,12 +1336,14 @@ "user_usage_stats_description": "கணக்கு உபயோகப் புள்ளிவிவரங்களைப் பார்க்க", "username": "பயனர்பெயர்", "users": "பயனர்கள்", + "users_added_to_album_count": "ஆல்பத்தில் {எண்ணிக்கை, பன்மை, ஒன்று{# user} மற்றவை{# users}} சேர்க்கப்பட்டது", "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": "ஒளிதோற்றம்", @@ -1300,21 +1355,32 @@ "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": "{ஆண்டுகள், பன்மை, ஒன்று {# ஆண்டு} மற்ற {# ஆண்டுகள்}}} முன்பு", "yes": "ஆம்", "you_dont_have_any_shared_links": "உங்களிடம் பகிரப்பட்ட இணைப்புகள் எதுவும் இல்லை", + "your_wifi_name": "உங்கள் வைஃபை பெயர்", "zoom_image": "பெரிதாக்க படம்" } diff --git a/i18n/tr.json b/i18n/tr.json index f9355564ad..9e3486d231 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -11,7 +11,7 @@ "activity_changed": "Etkinlik {enabled, select, true {etkin} other {devre dışı}}", "add": "Ekle", "add_a_description": "Açıklama ekle", - "add_a_location": "Lokasyon ekle", + "add_a_location": "Konum ekle", "add_a_name": "İsim ekle", "add_a_title": "Başlık ekle", "add_birthday": "Doğum günü ekle", @@ -28,6 +28,7 @@ "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", @@ -38,13 +39,13 @@ "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.", + "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.", @@ -54,12 +55,12 @@ "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?", @@ -76,7 +77,7 @@ "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ü", @@ -88,7 +89,7 @@ "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_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_quality": "Kalite", @@ -115,7 +116,7 @@ "library_scanning_enable_description": "Periyodik kütüphane taramasını etkinleştir", "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", @@ -126,7 +127,7 @@ "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_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.", @@ -169,37 +170,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ı", @@ -233,8 +234,8 @@ "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_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", @@ -257,23 +258,23 @@ "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", @@ -291,11 +292,11 @@ "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", @@ -354,18 +355,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_number_of_days_description": "Öğeleri kalıcı olarak silmeden önce çöp kutusunda tutma süresi (gün)", "trash_settings": "Çöp ayarları", "trash_settings_description": "Çöp 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ı:", @@ -377,7 +378,7 @@ "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" @@ -388,17 +389,19 @@ "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", @@ -423,13 +426,13 @@ "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_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", @@ -438,8 +441,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", @@ -460,6 +463,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", @@ -475,9 +479,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", @@ -488,7 +492,7 @@ "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_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", @@ -496,26 +500,29 @@ "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", @@ -527,7 +534,7 @@ "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", @@ -590,8 +597,6 @@ "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", @@ -640,32 +645,32 @@ "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", + "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_file_cache": "Dosya Önbelleği Temizle", - "clear_message": "Mesajı Temizle", - "clear_value": "Değeri 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ı", @@ -684,9 +689,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?", @@ -711,7 +716,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", @@ -725,9 +730,9 @@ "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", @@ -759,11 +764,11 @@ "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?", @@ -784,13 +789,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", @@ -807,12 +812,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ı", @@ -824,13 +829,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", @@ -843,6 +848,7 @@ "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:", @@ -870,7 +876,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", @@ -878,47 +884,47 @@ "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_loading_image": "Resim yüklenirken hata oluştu", "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_reset_pin_code": "Pin kodu sıfırlanamadı", - "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", @@ -927,7 +933,7 @@ "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", @@ -950,8 +956,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", @@ -971,19 +977,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", @@ -997,7 +1003,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", @@ -1039,14 +1045,14 @@ "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_setting_description": "Uygulamanın özelliklerini yönet", "file_name": "Dosya adı", @@ -1063,17 +1069,20 @@ "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'inizi mi unuttunuz?", + "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", @@ -1100,20 +1109,20 @@ "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": "Partner öğ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": "Partner öğ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": "Partner öğ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", @@ -1146,7 +1155,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", @@ -1161,9 +1170,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}}", @@ -1171,7 +1180,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", @@ -1181,7 +1190,7 @@ "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", @@ -1207,8 +1216,8 @@ "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_asset_cast_failed": "Sunucuya yüklenmemiş bir öğe yansıtılamaz", + "local_assets": "Yerel Öğeler", "local_network": "Yerel Wi-Fi", "local_network_sheet_info": "Uygulama belirlenmiş Wi-Fi ağını kullanırken bu URL üzerinden sunucuya bağlanacaktır", "location_permission": "Konum izni", @@ -1239,15 +1248,15 @@ "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?", "longitude": "Boylam", @@ -1257,6 +1266,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", @@ -1265,7 +1275,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", @@ -1284,7 +1294,7 @@ "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_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", @@ -1320,8 +1330,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", @@ -1342,7 +1352,7 @@ "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_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ı", @@ -1357,7 +1367,7 @@ "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_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", @@ -1370,7 +1380,7 @@ "no_uploads_in_progress": "Yükleme işlemi 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.", @@ -1383,6 +1393,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", @@ -1394,13 +1405,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", @@ -1424,8 +1437,8 @@ "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}}", @@ -1445,10 +1458,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", @@ -1460,6 +1473,8 @@ "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 {}}", @@ -1500,6 +1515,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", @@ -1538,6 +1554,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", @@ -1545,10 +1562,12 @@ "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", "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", @@ -1568,16 +1587,16 @@ "refreshing_metadata": "Meta veriler yenileniyor", "regenerating_thumbnails": "Küçük resimler yeniden oluşturuluyor", "remote": "Uzaktan", - "remote_assets": "Uzak Varlıklar", + "remote_assets": "Uzak Öğeler", "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.", @@ -1589,28 +1608,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_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", @@ -1619,8 +1638,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", @@ -1687,7 +1707,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...", @@ -1714,6 +1734,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ı", @@ -1723,7 +1744,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", @@ -1731,7 +1752,7 @@ "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_title": "Orijinal görüntüyü göster", @@ -1759,7 +1780,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...", @@ -1779,7 +1800,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", @@ -1791,7 +1812,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", @@ -1807,10 +1828,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", @@ -1821,7 +1842,7 @@ "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", + "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", @@ -1868,7 +1889,7 @@ "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", @@ -1891,27 +1912,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", @@ -1922,7 +1945,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 önemli ölçüde daha yüksek ağ yüküne sebep olur", "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", @@ -1931,7 +1954,7 @@ "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", @@ -1944,17 +1967,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_no_assets": "Çöp kutusuna atılmış öğe yok", "trash_page_restore_all": "Tümünü geri yükle", - "trash_page_select_assets_btn": "İçerik seç", + "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ı", @@ -1962,8 +1986,8 @@ "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", @@ -1982,25 +2006,26 @@ "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", @@ -2012,7 +2037,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", @@ -2029,10 +2054,10 @@ "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", @@ -2048,9 +2073,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", diff --git a/i18n/uk.json b/i18n/uk.json index e9d7b5630f..c87ef4b752 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -39,7 +39,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 +70,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": "Повнорозмірне зображення з видаленими метаданими, які використовуються під час збільшення", @@ -261,7 +261,7 @@ "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": "Увімкнено перевірку хешу", @@ -353,11 +353,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 для кожного користувача, і цю дію не можна буде скасувати.", @@ -389,7 +389,7 @@ "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": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте цей параметр, щоб завантажувати зображення з серверу.", @@ -491,11 +491,11 @@ "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_uploaded": "Завантажено", "asset_uploading": "Завантаження…", "asset_viewer_settings_subtitle": "Керуйте налаштуваннями переглядача галереї", @@ -503,26 +503,26 @@ "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, 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_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", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Управління налаштуваннями завантаження у фоновому та активному режимі", "backup_settings_subtitle": "Керування налаштуваннями завантаження", "backward": "Зворотній", - "beta_sync": "Стан бета-синхронізації", - "beta_sync_subtitle": "Налаштування нової системи синхронізації", "biometric_auth_enabled": "Біометрична автентифікація увімкнена", "biometric_locked_out": "Вам закрито доступ до біометричної автентифікації", "biometric_no_options": "Біометричні параметри недоступні", @@ -611,12 +609,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": "Повнорзомірні зображення", @@ -656,9 +654,9 @@ "change_pin_code": "Змінити PIN-код", "change_your_password": "Змініть свій пароль", "changed_visibility_successfully": "Видимість успішно змінено", - "check_corrupt_asset_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": "Місто", @@ -691,7 +689,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-код", @@ -732,7 +730,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": "Вибрати фото", @@ -797,7 +795,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": "Помилка оновлення опису, перевірте логи для подробиць", @@ -877,8 +875,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-код, щоб увімкнути біометричну автентифікацію", @@ -890,7 +888,7 @@ "enter_your_pin_code_subtitle": "Введіть свій PIN-код, щоб отримати доступ до особистої папки", "error": "Помилка", "error_change_sort_album": "Не вдалося змінити порядок сортування альбому", - "error_delete_face": "Помилка при видаленні обличчя з активу", + "error_delete_face": "Помилка при видаленні обличчя з елементу", "error_loading_image": "Помилка завантаження зображення", "error_saving_image": "Помилка: {error}", "error_tag_face_bounding_box": "Помилка під час позначення обличчя – не вдалося отримати координати рамки", @@ -967,7 +965,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": "Не вдалося отримати кількість коментарів", @@ -991,7 +989,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": "Не вдається зберегти альбом", @@ -1005,7 +1003,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": "Неможливо оновити обкладинку альбому", @@ -1043,7 +1041,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": "Помилка автентифікації", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast'", "gcast_enabled_description": "Ця функція завантажує зовнішні ресурси з Google для своєї роботи.", "general": "Загальні", - "geolocation_instruction_all_have_location": "Усі об’єкти для цієї дати вже мають дані про місцезнаходження. Спробуйте показати всі об’єкти або виберіть іншу дату", "geolocation_instruction_location": "Натисніть на об'єкт із GPS-координатами, щоб використати його місцезнаходження, або виберіть місцезнаходження безпосередньо на карті", - "geolocation_instruction_no_date": "Виберіть дату для керування даними про місцезнаходження для фотографій і відео за цей день", - "geolocation_instruction_no_photos": "Для цієї дати не знайдено фотографій чи відео. Виберіть іншу дату, щоб показати їх", "get_help": "Отримати допомогу", "get_wifiname_error": "Не вдалося отримати назву Wi-Fi. Переконайтеся, що ви надали необхідні дозволи та підключені до Wi-Fi мережі", "getting_started": "Початок", @@ -1160,7 +1155,7 @@ "in_archive": "В архіві", "include_archived": "Відображати архів", "include_shared_albums": "Включити спільні альбоми", - "include_shared_partner_assets": "Включайте спільні партнерські активи", + "include_shared_partner_assets": "Включайте спільні партнерські ресурси", "individual_share": "Індивідуальний доступ", "individual_shares": "Окремі спільні доступи", "info": "Інформація", @@ -1224,7 +1219,7 @@ "local_asset_cast_failed": "Неможливо транслювати ресурс, який не завантажено на сервер", "local_assets": "Локальні фото та відео", "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": "Обрати на мапі", @@ -1335,9 +1330,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": "Приглушити спогади", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Відкрийте фільтри пошуку", "options": "Налаштування", "or": "або", + "organize_into_albums": "Упорядкувати в альбоми", + "organize_into_albums_description": "Помістити наявні фотографії в альбоми, використовуючи поточні налаштування синхронізації", "organize_your_library": "Організуйте свою бібліотеку", "original": "оригінал", "other": "Інше", @@ -1514,11 +1511,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_readonly_mode": "Режим лише для читання ввімкнено. Щоб вийти, довго натисніть значок аватара користувача.", "profile_drawer_server_out_of_date_major": "Сервер застарів. Будь ласка, оновіть до останньої мажорної версії.", "profile_drawer_server_out_of_date_minor": "Сервер застарів. Будь ласка, оновіть до останньої мінорної версії.", "profile_image_of_user": "Зображення профілю {user}", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Статус підтримки", "purchase_server_title": "Сервер", "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором", + "query_asset_id": "Ідентифікатор ресурсу запиту", "queue_status": "У черзі {count} з {total}", "rating": "Зоряний рейтинг", "rating_clear": "Очистити рейтинг", @@ -1614,7 +1612,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": "Невідстежувані та відсутні файли будуть відображені тут", @@ -1638,10 +1636,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": "Перегляд великих файлів", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "Не вдалося створити альбом", "selected": "Обрано", "selected_count": "{count, plural, one {# обраний} other {# обраних}}", - "selected_gps_coordinates": "вибрані GPS-координати", + "selected_gps_coordinates": "Вибрані GPS-координати", "send_message": "Надіслати повідомлення", "send_welcome_email": "Надішліть вітальний лист", "server_endpoint": "Кінцева точка сервера", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "натисніть ⇧ щоб видалити об'єкт назавжди", "show_album_options": "Показати параметри альбому", "show_albums": "Показувати альбоми", - "show_all_assets": "Показати всі ресурси", "show_all_people": "Показати всіх людей", "show_and_hide_people": "Показати та приховати людей", - "show_assets_without_location": "Показати активи без місцезнаходження", "show_file_location": "Показати розташування файлу", "show_gallery": "Показати галерею", "show_hidden_people": "Показати прихованих людей", @@ -1920,6 +1917,8 @@ "sync_albums_manual_subtitle": "Синхронізувати всі завантажені фото та відео у вибрані альбоми для резервного копіювання", "sync_local": "Синхронізувати на пристрої", "sync_remote": "Синхронізувати з сервером", + "sync_status": "Стан синхронізації", + "sync_status_subtitle": "Перегляд та керування системою синхронізації", "sync_upload_album_setting_subtitle": "Створюйте та завантажуйте свої фотографії та відео до вибраних альбомів на сервер Immich", "tag": "Тег", "tag_assets": "Додати теги", @@ -1928,7 +1927,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": "Шаблон", @@ -1960,25 +1959,26 @@ "to_multi_select": "для багаторазового вибору", "to_parent": "Повернутись назад", "to_select": "вибрати", - "to_trash": "Смітник", + "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-код", @@ -2034,7 +2034,6 @@ "use_biometric": "Використовувати біометрію", "use_current_connection": "використовувати поточне підключення", "use_custom_date_range": "Використовувати користувацький діапазон дат", - "use_this_location": "Натисніть, щоб використовувати місцезнаходження", "user": "Користувач", "user_has_been_deleted": "Користувача видалено.", "user_id": "ID Користувача", @@ -2077,6 +2076,7 @@ "view_next_asset": "Переглянути наступний ресурс", "view_previous_asset": "Переглянути попередній ресурс", "view_qr_code": "Переглянути QR-код", + "view_similar_photos": "Переглянути схожі фотографії", "view_stack": "Перегляд стеку", "view_user": "Переглянути користувача", "viewer_remove_from_stack": "Видалити зі стеку", diff --git a/i18n/vi.json b/i18n/vi.json index 49a73f022c..fe25f7dab8 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -558,8 +558,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 c0025b82e1..7e273faa0b 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -396,6 +396,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": "當在網頁端執行刪除或還原操作時,自動在此裝置上刪除或還原該媒體", @@ -577,7 +579,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 +596,6 @@ "backup_setting_subtitle": "管理背景與前台上傳設定", "backup_settings_subtitle": "管理上傳設定", "backward": "由舊至新", - "beta_sync": "測試版同步狀態", - "beta_sync_subtitle": "管理新的同步系統", "biometric_auth_enabled": "生物辨識驗證已啟用", "biometric_locked_out": "您已被鎖定無法使用生物辨識驗證", "biometric_no_options": "沒有生物辨識選項可用", @@ -1073,6 +1073,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "此功能需要從 Google 載入外部資源才能正常運作。", "general": "一般", + "geolocation_instruction_location": "點擊具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點", "get_help": "線上求助", "get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連接至 Wi-Fi 網路", "getting_started": "開始使用", @@ -1262,6 +1263,7 @@ "main_branch_warning": "您現在使用的是開發版本;我們強烈您建議使用正式發行版!", "main_menu": "主頁面", "make": "製造商", + "manage_geolocation": "管理位置", "manage_shared_links": "管理共享連結", "manage_sharing_with_partners": "管理與親朋好友的分享", "manage_the_app_settings": "管理應用程式設定", @@ -1468,7 +1470,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": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", @@ -1508,6 +1510,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 +1549,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 +1557,8 @@ "rating_description": "在資訊面板中顯示 EXIF 評等", "reaction_options": "反應選項", "read_changelog": "閱覽變更日誌", + "readonly_mode_disabled": "唯讀模式已關閉", + "readonly_mode_enabled": "唯讀模式已開啟", "reassign": "重新指定", "reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個檔案}}重新指定給{name, select, null {現有的人} other {{name}}}", "reassigned_assets_to_new_person": "已將 {count, plural, other {# 個檔案}}重新指定給一位新人物", @@ -1629,6 +1635,7 @@ "restore_user": "還原使用者", "restored_asset": "已還原檔案", "resume": "繼續", + "resume_paused_jobs": "恢復 {count, plural, one {# 暫停的任務} other {# 暫停的任務}}", "retry_upload": "重新上傳", "review_duplicates": "檢視重複項目", "review_large_files": "檢視大型文件", @@ -1722,6 +1729,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": "伺服器端點", @@ -1904,6 +1912,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 +1951,9 @@ "to_change_password": "變更密碼", "to_favorite": "收藏", "to_login": "登入", + "to_multi_select": "進行多選", "to_parent": "到上一級", + "to_select": "选择", "to_trash": "垃圾桶", "toggle_settings": "切換設定", "total": "統計", @@ -1961,6 +1973,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 +2004,7 @@ "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", "untagged": "無標籤", "up_next": "下一個", + "update_location_action_prompt": "使用以下命令更新{count}個所選資產的位置:", "updated_at": "更新於", "updated_password": "已更新密碼", "upload": "上傳", @@ -2057,6 +2071,7 @@ "view_next_asset": "查看下一項", "view_previous_asset": "查看上一項", "view_qr_code": "查看 QR code", + "view_similar_photos": "查看相似照片", "view_stack": "查看堆疊", "view_user": "顯示使用者", "viewer_remove_from_stack": "從堆疊中移除", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index dc1ee9ff2f..df9ca98e85 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -26,8 +26,8 @@ "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_toggle": "选择相册 {album}", "add_to_albums": "添加到相册", "add_to_albums_count": "添加到相册({count}个)", @@ -100,7 +100,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 +121,13 @@ "library_watching_settings": "监控图库(实验性)", "library_watching_settings_description": "自动监控文件变化", "logging_enable_description": "启用日志记录", - "logging_level_description": "启用的日志级别。", + "logging_level_description": "启用时,要使用的日志级别。", "logging_settings": "日志", "machine_learning_clip_model": "CLIP 模型", "machine_learning_clip_model_description": "请于 此处查看支持的 CLIP 模型名称。注意,更换模型后需要对所有图片重新运行“智能搜索”任务。", "machine_learning_duplicate_detection": "重复项检测", "machine_learning_duplicate_detection_enabled": "启用重复检测", - "machine_learning_duplicate_detection_enabled_description": "如果禁用此功能,完全相同的项目仍将被去重。", + "machine_learning_duplicate_detection_enabled_description": "如果禁用,完全相同的项目仍将被去重。", "machine_learning_duplicate_detection_setting_description": "使用 CLIP 向量匹配(关键词相似度)来查找可能的重复项", "machine_learning_enabled": "启用机器学习", "machine_learning_enabled_description": "如果禁用,无论以下如何设置,所有机器学习功能将被禁用。", @@ -158,7 +158,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 +220,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 +364,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": "用户详情", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "管理后台和前台上传设置", "backup_settings_subtitle": "管理上传设置", "backward": "后退", - "beta_sync": "测试版同步状态", - "beta_sync_subtitle": "管理新的同步系统", "biometric_auth_enabled": "生物识别身份验证已启用", "biometric_locked_out": "您被锁定在生物识别身份验证之外", "biometric_no_options": "没有可用的生物识别选项", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast 投屏", "gcast_enabled_description": "该功能需要加载来自 Google 的外部资源。", "general": "通用", - "geolocation_instruction_all_have_location": "此日期的所有资产都已具有位置数据。尝试显示所有资产或选择其他日期", "geolocation_instruction_location": "点击带有GPS坐标的资产以使用其位置,或直接从地图上选择位置", - "geolocation_instruction_no_date": "选择一个日期来管理当天照片和视频的位置数据", - "geolocation_instruction_no_photos": "没有找到此日期的照片或视频。选择其他日期显示它们", "get_help": "获取帮助", "get_wifiname_error": "无法获取 Wi-Fi 名称。确保已授予必要的权限,并已连接到 Wi-Fi 网络", "getting_started": "入门", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "打开搜索过滤器", "options": "选项", "or": "或", + "organize_into_albums": "整理成相册", + "organize_into_albums_description": "使用当前同步设置将现有照片放入相册", "organize_your_library": "整理您的图库", "original": "原图", "other": "其它", @@ -1518,7 +1515,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_readonly_mode": "只读模式已启用。长按用户头像图标退出。", "profile_drawer_server_out_of_date_major": "服务端有大版本升级,请尽快升级至最新版。", "profile_drawer_server_out_of_date_minor": "服务端有小版本升级,请尽快升级至最新版。", "profile_image_of_user": "{user}的个人资料图片", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "支持者状态", "purchase_server_title": "服务器", "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", + "query_asset_id": "查询资产ID", "queue_status": "排队中 {count}/{total}", "rating": "星级", "rating_clear": "删除星级", @@ -1642,6 +1640,7 @@ "restore_user": "恢复用户", "restored_asset": "已恢复项目", "resume": "继续", + "resume_paused_jobs": "继续 {count, plural, one {# 已暂停的任务} other {# 已暂停的任务}}", "retry_upload": "重新上传", "review_duplicates": "检查重复项", "review_large_files": "查看大文件", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "创建相册失败", "selected": "已选择", "selected_count": "{count, plural, other {#项已选择}}", - "selected_gps_coordinates": "选定的GPS坐标", + "selected_gps_coordinates": "已选定的GPS坐标", "send_message": "发送消息", "send_welcome_email": "发送欢迎邮件", "server_endpoint": "服务器 URL", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "按住 ⇧ Shift 键永久删除项目", "show_album_options": "显示相册选项", "show_albums": "显示相册", - "show_all_assets": "显示所有资产", "show_all_people": "显示所有人物", "show_and_hide_people": "显示和隐藏人物", - "show_assets_without_location": "显示不带GPS定位的资产", "show_file_location": "显示文件位置", "show_gallery": "显示图库", "show_hidden_people": "显示隐藏人物", @@ -1920,6 +1917,8 @@ "sync_albums_manual_subtitle": "将所有上传的视频和照片同步到选定的备份相册", "sync_local": "同步本地", "sync_remote": "同步远程", + "sync_status": "同步状态", + "sync_status_subtitle": "查看和管理同步系统", "sync_upload_album_setting_subtitle": "创建照片和视频并上传到 Immich 上的选定相册中", "tag": "标签", "tag_assets": "标记项目", @@ -1930,7 +1929,7 @@ "tag_updated": "已更新标签:{tag}", "tagged_assets": "{count, plural, one {# 个项目} other {# 个项目}}被加上标签", "tags": "标签", - "tap_to_run_job": "点击运行作业", + "tap_to_run_job": "点击运行任务", "template": "模版", "theme": "主题", "theme_selection": "主题选项", @@ -1979,6 +1978,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码", @@ -2034,7 +2034,6 @@ "use_biometric": "使用生物识别", "use_current_connection": "使用当前连接", "use_custom_date_range": "自定义日期范围", - "use_this_location": "单击以使用此位置", "user": "用户", "user_has_been_deleted": "此用户已被删除。", "user_id": "用户 ID", @@ -2077,6 +2076,7 @@ "view_next_asset": "查看下一项", "view_previous_asset": "查看上一项", "view_qr_code": "查看二维码", + "view_similar_photos": "查看相似照片", "view_stack": "查看堆叠项目", "view_user": "查看用户", "viewer_remove_from_stack": "从堆叠中移除", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 9eba51c99c..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:f3660c56d5b08d6c516360981bedc439f499b9bf37f46a216018da3777a74011 /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/uv.lock b/machine-learning/uv.lock index fa54a13e8c..393dabe319 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -1341,7 +1341,7 @@ wheels = [ [[package]] name = "locust" -version = "2.39.1" +version = "2.40.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1353,6 +1353,7 @@ dependencies = [ { name = "locust-cloud" }, { name = "msgpack" }, { name = "psutil" }, + { name = "pytest" }, { name = "python-engineio" }, { name = "python-socketio", extra = ["client"] }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -1360,12 +1361,12 @@ dependencies = [ { 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/95/c8/10aa5445c404eed389b56877e6714c1787190cc09dd70059ce3765979ec5/locust-2.39.1.tar.gz", hash = "sha256:6bdd19e27edf9a1c84391d6cf6e9a737dfb832be7dfbf39053191ae31b9cc498", size = 1409902, upload-time = "2025-08-29T17:41:01.544Z" } +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/ec/b3/b2f4b2ca88b1e72eba7be2b2982533b887f8b709d222db78eb9602aa5121/locust-2.39.1-py3-none-any.whl", hash = "sha256:fd5148f2f1a4ed34aee968abc4393674e69d1b5e1b54db50a397f6eb09ce0b04", size = 1428155, upload-time = "2025-08-29T17:41:00.245Z" }, + { 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]] @@ -2204,7 +2205,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'" }, @@ -2215,9 +2216,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]] @@ -2249,14 +2250,14 @@ wheels = [ [[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]] @@ -2532,28 +2533,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.11" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } +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/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, - { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, - { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, - { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, - { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, + { 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]] @@ -2876,27 +2877,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]] diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000000..9aae0b9572 --- /dev/null +++ b/mise.lock @@ -0,0 +1,34 @@ +[tools.dart] +version = "3.8.2" +backend = "asdf:dart" + +[tools.flutter] +version = "3.35.3-stable" +backend = "asdf:flutter" + +[tools."github:CQLabs/homebrew-dcm"] +version = "1.31.4" +backend = "github:CQLabs/homebrew-dcm" + +[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64] +checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955" +size = 8821083 +url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip" + +[tools.node] +version = "22.18.0" +backend = "core:node" + +[tools.node.platforms.linux-x64] +checksum = "sha256:a2e703725d8683be86bb5da967bf8272f4518bdaf10f21389e2b2c9eaeae8c8a" +size = 54824343 +url = "https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz" + +[tools.pnpm] +version = "10.14.0" +backend = "aqua:pnpm/pnpm" + +[tools.pnpm.platforms.linux-x64] +checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d" +size = 66231525 +url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..ed287b178d --- /dev/null +++ b/mise.toml @@ -0,0 +1,312 @@ +[tools] +node = "22.19.0" +flutter = "3.35.3" +pnpm = "10.14.0" +dart = "3.8.2" + +[tools."github:CQLabs/homebrew-dcm"] +version = "1.31.4" +bin = "dcm" +postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm" + +[settings] +experimental = true +lockfile = 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", +] diff --git a/mobile/.fvmrc b/mobile/.fvmrc index 3ca65ffc7c..d8a11548fd 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.32.8" + "flutter": "3.35.3" } \ No newline at end of file diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 9a9fb67ce3..0c2f2c29cc 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.3", "dart.lineLength": 120, "[dart]": { - "editor.rulers": [120], + "editor.rulers": [120] }, "search.exclude": { "**/.fvm": true diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index bef051bff2..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 @@ -134,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 4237643233..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,6 +3,7 @@ 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() { @@ -14,6 +15,8 @@ class ImmichApp : Application() { // 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 a87feddd1a..4e811c8dfc 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 @@ -3,8 +3,11 @@ package app.alextran.immich import android.content.Context import android.os.Build import android.os.ext.SdkExtensions +import app.alextran.immich.background.BackgroundEngineLock import app.alextran.immich.background.BackgroundWorkerApiImpl import app.alextran.immich.background.BackgroundWorkerFgHostApi +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 @@ -23,6 +26,7 @@ class MainActivity : FlutterFragmentActivity() { fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) { flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(HttpSSLOptionsPlugin()) + flutterEngine.plugins.add(BackgroundEngineLock()) val messenger = flutterEngine.dartExecutor.binaryMessenger val nativeSyncApiImpl = @@ -34,6 +38,7 @@ class MainActivity : FlutterFragmentActivity() { NativeSyncApi.setUp(messenger, nativeSyncApiImpl) ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx)) BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) + ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) } } } 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..6d6f45a708 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt @@ -0,0 +1,33 @@ +package app.alextran.immich.background + +import android.util.Log +import androidx.work.WorkManager +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.plugins.FlutterPlugin +import java.util.concurrent.atomic.AtomicInteger + +private const val TAG = "BackgroundEngineLock" + +class BackgroundEngineLock : FlutterPlugin { + companion object { + const val ENGINE_CACHE_KEY = "immich::background_worker::engine" + var engineCount = AtomicInteger(0) + } + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + // work manager task is running while the main app is opened, cancel the worker + if (engineCount.incrementAndGet() > 1 && FlutterEngineCache.getInstance() + .get(ENGINE_CACHE_KEY) != null + ) { + WorkManager.getInstance(binding.applicationContext) + .cancelUniqueWork(BackgroundWorkerApiImpl.BACKGROUND_WORKER_NAME) + FlutterEngineCache.getInstance().remove(ENGINE_CACHE_KEY) + } + 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 index 39a2345a9b..052395c172 100644 --- 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 @@ -37,6 +37,36 @@ private object BackgroundWorkerPigeonUtils { ) } } + 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 + } + } /** @@ -50,20 +80,64 @@ class FlutterError ( 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 super.readValueOfType(type, buffer) + 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?) { - super.writeValue(stream, value) + 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 enableSyncWorker() - fun enableUploadWorker(callbackHandle: Long) - fun disableUploadWorker() + fun enable() + fun configure(settings: BackgroundWorkerSettings) + fun disable() companion object { /** The codec used by BackgroundWorkerFgHostApi. */ @@ -75,11 +149,11 @@ interface BackgroundWorkerFgHostApi { 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.enableSyncWorker$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.enableSyncWorker() + api.enable() listOf(null) } catch (exception: Throwable) { BackgroundWorkerPigeonUtils.wrapError(exception) @@ -91,13 +165,13 @@ interface BackgroundWorkerFgHostApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec) + 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 callbackHandleArg = args[0] as Long + val settingsArg = args[0] as BackgroundWorkerSettings val wrapped: List = try { - api.enableUploadWorker(callbackHandleArg) + api.configure(settingsArg) listOf(null) } catch (exception: Throwable) { BackgroundWorkerPigeonUtils.wrapError(exception) @@ -109,11 +183,11 @@ interface BackgroundWorkerFgHostApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.disableUploadWorker() + api.disable() listOf(null) } catch (exception: Throwable) { BackgroundWorkerPigeonUtils.wrapError(exception) @@ -130,6 +204,8 @@ interface BackgroundWorkerFgHostApi { /** 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. */ @@ -156,6 +232,41 @@ interface BackgroundWorkerBgHostApi { 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) + } + } } } } @@ -167,23 +278,6 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p BackgroundWorkerPigeonCodec() } } - fun onLocalSync(maxSecondsArg: Long?, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(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 onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" 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 index 0ce601b363..7d30319af4 100644 --- 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 @@ -1,27 +1,31 @@ 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.dart.DartExecutor.DartCallback +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.view.FlutterCallbackInformation +import java.util.concurrent.TimeUnit private const val TAG = "BackgroundWorker" -enum class BackgroundTaskType { - LOCAL_SYNC, - UPLOAD, -} - class BackgroundWorker(context: Context, params: WorkerParameters) : ListenableWorker(context, params), BackgroundWorkerBgHostApi { private val ctx: Context = context.applicationContext @@ -46,36 +50,35 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : /// Flag to track whether the background task has completed to prevent duplicate completions private var isComplete = false - init { - if (!loader.initialized()) { - loader.startInitialization(ctx) - } + 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) - - // Retrieve the callback handle stored by the main Flutter app - // This handle points to the Flutter function that should be executed in the background - val callbackHandle = - ctx.getSharedPreferences(BackgroundWorkerApiImpl.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(BackgroundWorkerApiImpl.SHARED_PREF_CALLBACK_HANDLE, 0L) - - if (callbackHandle == 0L) { - // Without a valid callback handle, we cannot start the Flutter background execution - complete(Result.failure()) - return@ensureInitializationCompleteAsync - } - - // Start the Flutter engine with the specified callback as the entry point - val callback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) - if (callback == null) { - complete(Result.failure()) - return@ensureInitializationCompleteAsync - } + FlutterEngineCache.getInstance().remove(BackgroundEngineLock.ENGINE_CACHE_KEY); + FlutterEngineCache.getInstance() + .put(BackgroundEngineLock.ENGINE_CACHE_KEY, engine!!) // Register custom plugins MainActivity.registerPlugins(ctx, engine!!) @@ -86,8 +89,12 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : api = this ) - engine!!.dartExecutor.executeDartCallback( - DartCallback(ctx.assets, loader.findAppBundlePath(), callback) + engine!!.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint( + loader.findAppBundlePath(), + "package:immich_mobile/domain/services/background_worker.service.dart", + "backgroundSyncNativeEntrypoint" + ) ) } @@ -100,23 +107,38 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : * This method acts as a bridge between the native Android background task system and Flutter. */ override fun onInitialized() { - val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0) - val taskType = BackgroundTaskType.entries[taskTypeIndex] + flutterApi?.onAndroidUpload { handleHostResult(it) } + } - when (taskType) { - BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) } - BackgroundTaskType.UPLOAD -> 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) } } - /** - * 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") - + override fun close() { if (isComplete) { return } @@ -129,11 +151,23 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : } } + 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 @@ -154,9 +188,38 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : * - 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 + FlutterEngineCache.getInstance().remove(BackgroundEngineLock.ENGINE_CACHE_KEY); flutterApi = null + notificationManager.cancel(NOTIFICATION_ID) + 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 index 7a3226f961..259e3244bc 100644 --- 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 @@ -1,69 +1,53 @@ package app.alextran.immich.background import android.content.Context +import android.content.SharedPreferences import android.provider.MediaStore import android.util.Log -import androidx.core.content.edit import androidx.work.BackoffPolicy import androidx.work.Constraints -import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import java.util.concurrent.TimeUnit -private const val TAG = "BackgroundUploadImpl" +private const val TAG = "BackgroundWorkerApiImpl" class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { private val ctx: Context = context.applicationContext - override fun enableSyncWorker() { + + override fun enable() { enqueueMediaObserver(ctx) - Log.i(TAG, "Scheduled media observer") } - override fun enableUploadWorker(callbackHandle: Long) { - updateUploadEnabled(ctx, true) - updateCallbackHandle(ctx, callbackHandle) - Log.i(TAG, "Scheduled background upload tasks") + override fun configure(settings: BackgroundWorkerSettings) { + BackgroundWorkerPreferences(ctx).updateSettings(settings) + enqueueMediaObserver(ctx) } - override fun disableUploadWorker() { - updateUploadEnabled(ctx, false) - WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME) + 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" + const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1" private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1" - const val WORKER_DATA_TASK_TYPE = "taskType" - - const val SHARED_PREF_NAME = "Immich::Background" - const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled" - const val SHARED_PREF_CALLBACK_HANDLE = "Background::backup::callbackHandle" - - private fun updateUploadEnabled(context: Context, enabled: Boolean) { - context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { - putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled) - } - } - - private fun updateCallbackHandle(context: Context, callbackHandle: Long) { - context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { - putLong(SHARED_PREF_CALLBACK_HANDLE, callbackHandle) - } - } - fun enqueueMediaObserver(ctx: Context) { - val constraints = Constraints.Builder() - .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(5, TimeUnit.SECONDS) - .setTriggerContentMaxDelay(1, TimeUnit.MINUTES) - .build() + 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.MINUTES) + setRequiresCharging(settings.requiresCharging) + }.build() val work = OneTimeWorkRequest.Builder(MediaObserver::class.java) .setConstraints(constraints) @@ -71,22 +55,53 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { WorkManager.getInstance(ctx) .enqueueUniqueWork(OBSERVER_WORKER_NAME, ExistingWorkPolicy.REPLACE, work) - Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME") + Log.i( + TAG, + "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME and settings: $settings" + ) } - fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) { + fun enqueueBackgroundWorker(ctx: Context) { val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build() - val data = Data.Builder() - data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal) val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setInputData(data.build()).build() + .build() WorkManager.getInstance(ctx) - .enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work) + .enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.KEEP, work) Log.i(TAG, "Enqueued background worker with name: $BACKGROUND_WORKER_NAME") } } } + +private class BackgroundWorkerPreferences(private val ctx: Context) { + companion object { + private 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 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().apply { + putLong(SHARED_PREF_MIN_DELAY_KEY, settings.minimumDelaySeconds) + putBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, settings.requiresCharging) + apply() + } + } + + fun getSettings(): BackgroundWorkerSettings { + return BackgroundWorkerSettings( + minimumDelaySeconds = sp.getLong(SHARED_PREF_MIN_DELAY_KEY, DEFAULT_MIN_DELAY_SECONDS), + requiresCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, DEFAULT_REQUIRE_CHARGING), + ) + } +} 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 index 0ec6eeb3a5..7283411ac0 100644 --- 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 @@ -6,29 +6,17 @@ import androidx.work.Worker import androidx.work.WorkerParameters class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) { - private val ctx: Context = context.applicationContext + private val ctx: Context = context.applicationContext - override fun doWork(): Result { - Log.i("MediaObserver", "Content change detected, starting background worker") + 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()) { - val type = - if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC - BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type) - } - - // Re-enqueue itself to listen for future changes - BackgroundWorkerApiImpl.enqueueMediaObserver(ctx) - return Result.success() - } - - private fun isBackupEnabled(context: Context): Boolean { - val prefs = - context.getSharedPreferences( - BackgroundWorkerApiImpl.SHARED_PREF_NAME, - Context.MODE_PRIVATE - ) - return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false) + // 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/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 108cb4fc8b..011658d09a 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" => 3011, - "android.injected.version.name" => "1.140.1", + "android.injected.version.code" => 3015, + "android.injected.version.name" => "1.142.1", } ) 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/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_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/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 14c542b068..48826a20f1 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -18,6 +18,8 @@ 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 */; }; @@ -97,6 +99,8 @@ 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; }; @@ -243,6 +247,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B25D37792E72CA15008B6CA7 /* Connectivity */, B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, @@ -271,6 +276,15 @@ path = Background; sourceTree = ""; }; + B25D37792E72CA15008B6CA7 /* Connectivity */ = { + isa = PBXGroup; + children = ( + B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */, + B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */, + ); + path = Connectivity; + sourceTree = ""; + }; FAC6F8B62D287F120078CB2F /* ShareExtension */ = { isa = PBXGroup; children = ( @@ -507,14 +521,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -543,14 +553,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -566,7 +572,9 @@ 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 */, @@ -697,7 +705,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -841,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -871,7 +879,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -905,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -948,7 +956,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -988,7 +996,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1027,7 +1035,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1071,7 +1079,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1112,7 +1120,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 224; 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 04422eb2b4..3476030923 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -24,7 +24,7 @@ import UIKit BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) BackgroundServicePlugin.registerBackgroundProcessing() - BackgroundWorkerApiImpl.registerBackgroundProcessing() + BackgroundWorkerApiImpl.registerBackgroundWorkers() BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index e9513db8da..ece5cd5f64 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -50,11 +50,119 @@ private func nilOrValue(_ value: Any?) -> T? { 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 { @@ -73,9 +181,9 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerFgHostApi { - func enableSyncWorker() throws - func enableUploadWorker(callbackHandle: Int64) throws - func disableUploadWorker() throws + func enable() throws + func configure(settings: BackgroundWorkerSettings) throws + func disable() throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -84,52 +192,54 @@ class BackgroundWorkerFgHostApiSetup { /// 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 enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - enableSyncWorkerChannel.setMessageHandler { _, reply in + enableChannel.setMessageHandler { _, reply in do { - try api.enableSyncWorker() + try api.enable() reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - enableSyncWorkerChannel.setMessageHandler(nil) + enableChannel.setMessageHandler(nil) } - let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let configureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - enableUploadWorkerChannel.setMessageHandler { message, reply in + configureChannel.setMessageHandler { message, reply in let args = message as! [Any?] - let callbackHandleArg = args[0] as! Int64 + let settingsArg = args[0] as! BackgroundWorkerSettings do { - try api.enableUploadWorker(callbackHandle: callbackHandleArg) + try api.configure(settings: settingsArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - enableUploadWorkerChannel.setMessageHandler(nil) + configureChannel.setMessageHandler(nil) } - let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - disableUploadWorkerChannel.setMessageHandler { _, reply in + disableChannel.setMessageHandler { _, reply in do { - try api.disableUploadWorker() + try api.disable() reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - disableUploadWorkerChannel.setMessageHandler(nil) + 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`. @@ -151,11 +261,39 @@ class BackgroundWorkerBgHostApiSetup { } 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 onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) func onAndroidUpload(completion: @escaping (Result) -> Void) func cancel(completion: @escaping (Result) -> Void) @@ -170,24 +308,6 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { var codec: BackgroundWorkerPigeonCodec { return BackgroundWorkerPigeonCodec.shared } - func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([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 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) diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index db849d942b..c3268b4a2b 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -1,7 +1,7 @@ import BackgroundTasks import Flutter -enum BackgroundTaskType { case localSync, refreshUpload, processingUpload } +enum BackgroundTaskType { case refresh, processing } /* * DEBUG: Testing Background Tasks in Xcode @@ -9,10 +9,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload } * 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 local sync (short-running sync): - - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"] ## For background refresh (short-running sync): @@ -24,8 +20,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload } * To simulate task expiration (useful for testing expiration handlers): - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"] - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"] @@ -86,28 +80,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * starts the engine, and sets up a timeout timer if specified. */ func run() { - // Retrieve the callback handle stored by the main Flutter app - // This handle points to the Flutter function that should be executed in the background - let callbackHandle = Int64(UserDefaults.standard.string( - forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) ?? "0") ?? 0 - - if callbackHandle == 0 { - // Without a valid callback handle, we cannot start the Flutter background execution - complete(success: false) - return - } - - // Use the callback handle to retrieve the actual Flutter callback information - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // The callback handle is invalid or the callback was not found - complete(success: false) - return - } - // Start the Flutter engine with the specified callback as the entry point let isRunning = engine.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath + withEntrypoint: "backgroundSyncNativeEntrypoint", + libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart" ) // Verify that the Flutter engine started successfully @@ -127,7 +103,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { if maxSeconds != nil { // Schedule a timer to cancel the task after the specified timeout period Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in - self.cancel() + self.close() } } } @@ -138,30 +114,25 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * This method acts as a bridge between the native iOS background task system and Flutter. */ func onInitialized() throws { - switch self.taskType { - case .refreshUpload, .processingUpload: - flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload, - maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in - self.handleHostResult(result: result) - }) - case .localSync: - flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in - self.handleHostResult(result: result) - }) - } + 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 cancel() { + func close() { if isComplete { return } - isComplete = true flutterApi?.cancel { result in self.complete(success: false) } @@ -172,6 +143,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { self.complete(success: false) } } + /** * Handles the result from Flutter API calls and determines the success/failure status. @@ -182,7 +154,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { private func handleHostResult(result: Result) { switch result { case .success(): self.complete(success: true) - case .failure(_): self.cancel() + case .failure(_): self.close() } } @@ -195,8 +167,13 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * - 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 index f36085de0b..ca2453404c 100644 --- a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -1,84 +1,45 @@ import BackgroundTasks class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { - func enableSyncWorker() throws { - BackgroundWorkerApiImpl.scheduleLocalSync() - print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled") + + func enable() throws { + BackgroundWorkerApiImpl.scheduleRefreshWorker() + BackgroundWorkerApiImpl.scheduleProcessingWorker() + print("BackgroundUploadImpl:enbale Background worker scheduled") } - func enableUploadWorker(callbackHandle: Int64) throws { - BackgroundWorkerApiImpl.updateUploadEnabled(true) - // Store the callback handle for later use when starting background Flutter isolates - BackgroundWorkerApiImpl.updateUploadCallbackHandle(callbackHandle) - - BackgroundWorkerApiImpl.scheduleRefreshUpload() - BackgroundWorkerApiImpl.scheduleProcessingUpload() - print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks") + func configure(settings: BackgroundWorkerSettings) throws { + // Android only } - func disableUploadWorker() throws { - BackgroundWorkerApiImpl.updateUploadEnabled(false) - BackgroundWorkerApiImpl.cancelUploadTasks() - print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks") + func disable() throws { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); + print("BackgroundUploadImpl:disableUploadWorker Disabled background workers") } - public static let backgroundUploadEnabledKey = "immich:background:backup:enabled" - public static let backgroundUploadCallbackHandleKey = "immich:background:backup:callbackHandle" - - private static let localSyncTaskID = "app.alextran.immich.background.localSync" - private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload" - private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload" + 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) - private static func updateUploadEnabled(_ isEnabled: Bool) { - return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey) - } - - private static func updateUploadCallbackHandle(_ callbackHandle: Int64) { - return UserDefaults.standard.set(String(callbackHandle), forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) - } - - private static func cancelUploadTasks() { - BackgroundWorkerApiImpl.updateUploadEnabled(false) - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID); - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID); - } - - public static func registerBackgroundProcessing() { + public static func registerBackgroundWorkers() { BGTaskScheduler.shared.register( - forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in + forTaskWithIdentifier: processingTaskID, using: nil) { task in if task is BGProcessingTask { handleBackgroundProcessing(task: task as! BGProcessingTask) } } BGTaskScheduler.shared.register( - forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in + forTaskWithIdentifier: refreshTaskID, using: nil) { task in if task is BGAppRefreshTask { - handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload) + handleBackgroundRefresh(task: task as! BGAppRefreshTask) } } - - BGTaskScheduler.shared.register( - forTaskWithIdentifier: localSyncTaskID, using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync) - } - } } - private static func scheduleLocalSync() { - let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID) - backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins - - do { - try BGTaskScheduler.shared.submit(backgroundRefresh) - } catch { - print("Could not schedule the local sync task \(error.localizedDescription)") - } - } - - private static func scheduleRefreshUpload() { - let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID) + private static func scheduleRefreshWorker() { + let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID) backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins do { @@ -88,8 +49,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } } - private static func scheduleProcessingUpload() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID) + private static func scheduleProcessingWorker() { + let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID) backgroundProcessing.requiresNetworkConnectivity = true backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins @@ -101,16 +62,22 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } } - private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) { - scheduleRefreshUpload() - // Restrict the refresh task to run only for a maximum of 20 seconds - runBackgroundWorker(task: task, taskType: taskType, maxSeconds: 20) + 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) { - scheduleProcessingUpload() + scheduleProcessingWorker() + taskSemaphore.wait() // There are no restrictions for processing tasks. Although, the OS could signal expiration at any time - runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil) + runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil) } /** @@ -124,6 +91,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { * - 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 @@ -134,7 +102,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { task.expirationHandler = { DispatchQueue.main.async { - backgroundWorker.cancel() + backgroundWorker.close() } isSuccess = false 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..d1ea2cc0e0 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)])) } } @@ -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 2c4473c039..c1e6ec212d 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -1,190 +1,189 @@ - - AppGroupId - $(CUSTOM_GROUP_ID) - BGTaskSchedulerPermittedIdentifiers - - app.alextran.immich.background.localSync - 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 - 1.140.0 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - Share Extension - CFBundleURLSchemes - - ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - - - - CFBundleTypeRole - Editor - CFBundleURLName - Deep Link - CFBundleURLSchemes - - immich - - - - CFBundleVersion - 219 - 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 + + 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 + 1.142.1 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + Share Extension + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + + CFBundleTypeRole + Editor + CFBundleURLName + Deep Link + CFBundleURLSchemes + + immich + + + + CFBundleVersion + 224 + 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 - - - \ No newline at end of file + 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/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 87838f2e5e..3f0edf6ae4 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.140.1" + version_number: "1.142.1" ) 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..60c9b92cf9 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -45,3 +45,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 a31811d1bd..dcda8d01cf 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -77,7 +77,10 @@ enum StoreKey { 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 aaf6d81934..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, @@ -99,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 && 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..a5fb5dd70e 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,6 +26,14 @@ 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); } @@ -62,7 +69,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 { @@ -89,4 +96,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 index 33c58cf743..d57fe507d9 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -1,10 +1,17 @@ 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/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'; @@ -13,15 +20,20 @@ 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/server_info.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; @@ -29,13 +41,20 @@ class BackgroundWorkerFgService { const BackgroundWorkerFgService(this._foregroundHostApi); // TODO: Move this call to native side once old timeline is removed - Future enableSyncService() => _foregroundHostApi.enableSyncWorker(); + Future enable() => _foregroundHostApi.enable(); - Future enableUploadService() => _foregroundHostApi.enableUploadWorker( - PluginUtilities.getCallbackHandle(_backgroundSyncNativeEntrypoint)!.toRawHandle(), + 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 disableUploadService() => _foregroundHostApi.disableUploadWorker(); + Future disable() => _foregroundHostApi.disable(); } class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { @@ -44,7 +63,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; - final Logger _logger = Logger('BackgroundUploadBgService'); + final CancellationToken _cancellationToken = CancellationToken(); + final Logger _logger = Logger('BackgroundWorkerBgService'); bool _isCleanedUp = false; @@ -66,92 +86,95 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); Future init() async { - await loadTranslations(); - HttpSSLOptions.apply(applyNative: false); - await _ref.read(authServiceProvider).setOpenApiServiceEndpoint(); + try { + HttpSSLOptions.apply(applyNative: false); - // Initialize the file downloader - await 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), - ], - ); - await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false); - await FileDownloader().trackTasks(); - configureFileDownloaderNotifications(); + 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(), + ]); - // Notify the host that the background upload service has been initialized and is ready to use - await _backgroundHostApi.onInitialized(); + configureFileDownloaderNotifications(); + + if (Platform.isAndroid) { + await _backgroundHostApi.showNotification( + IntlKeys.uploading_media.t(), + IntlKeys.backup_background_service_in_progress_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 onLocalSync(int? maxSeconds) async { - _logger.info('Local background syncing started'); - final sw = Stopwatch()..start(); - - final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null; - await _syncAssets(hashTimeout: timeout, syncRemote: false); - - sw.stop(); - _logger.info("Local sync completed in ${sw.elapsed.inSeconds}s"); - } - - /* We do the following on Android upload - * - Sync local assets - * - Hash local assets 3 / 6 minutes - * - Sync remote assets - * - Check and requeue upload tasks - */ @override Future onAndroidUpload() async { - _logger.info('Android background processing started'); - final sw = Stopwatch()..start(); + try { + _logger.info('Android background processing started'); + final sw = Stopwatch()..start(); - await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); - await _handleBackup(processBulk: false); + await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); + await _handleBackup(); - await _cleanup(); - - sw.stop(); - _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); + sw.stop(); + _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); + } catch (error, stack) { + _logger.severe("Failed to complete Android background processing", error, stack); + } finally { + await _cleanup(); + } } - /* We do the following on background upload - * - Sync local assets - * - Hash local assets - * - Sync remote assets - * - Check and requeue upload tasks - * - * The native side will not send the maxSeconds value for processing tasks - */ @override Future onIosUpload(bool isRefresh, int? maxSeconds) async { - _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); - final sw = Stopwatch()..start(); + try { + _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); + final sw = Stopwatch()..start(); - final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); - await _syncAssets(hashTimeout: timeout); + final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); + await _syncAssets(hashTimeout: timeout); - final backupFuture = _handleBackup(); - if (maxSeconds != null) { - await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); - } else { - await backupFuture; + final backupFuture = _handleBackup(); + if (maxSeconds != null) { + await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); + } else { + await backupFuture; + } + + sw.stop(); + _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); + } catch (error, stack) { + _logger.severe("Failed to complete iOS background upload", error, stack); + } finally { + await _cleanup(); } - - await _cleanup(); - - sw.stop(); - _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); } @override Future cancel() async { - _logger.warning("Background upload cancelled"); - await _cleanup(); + _logger.warning("Background worker cancelled"); + try { + await _cleanup(); + } catch (error, stack) { + dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); + } } Future _cleanup() async { @@ -159,74 +182,108 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { return; } - _isCleanedUp = true; - await _ref.read(backgroundSyncProvider).cancel(); - await _ref.read(backgroundSyncProvider).cancelLocal(); - await _isar.close(); - await _drift.close(); - await _driftLogger.close(); - _ref.dispose(); - } + try { + final backgroundSyncManager = _ref.read(backgroundSyncProvider); + _isCleanedUp = true; + _ref.dispose(); - Future _handleBackup({bool processBulk = true}) async { - if (!_isBackupEnabled) { - return; - } + _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(), + backgroundSyncManager.cancelLocal(), + ]; - final currentUser = _ref.read(currentUserProvider); - if (currentUser == null) { - return; - } - - if (processBulk) { - return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); - } - - final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id); - if (activeTask.isNotEmpty) { - await _ref.read(uploadServiceProvider).resumeBackup(); - } else { - await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id); + if (_isar.isOpen) { + cleanupFutures.add(_isar.close()); + } + await Future.wait(cleanupFutures); + _logger.info("Background worker resources cleaned up"); + } catch (error, stack) { + dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); } } - Future _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async { - final futures = >[]; + Future _handleBackup() async { + await runZonedGuarded( + () async { + if (!_isBackupEnabled || _isCleanedUp) { + _logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine"); + return; + } - final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async { - if (_isCleanedUp) { - return; - } + _logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service"); - var hashFuture = _ref.read(backgroundSyncProvider).hashAssets(); - if (hashTimeout != null) { - hashFuture = hashFuture.timeout( - hashTimeout, - onTimeout: () { - // Consume cancellation errors as we want to continue processing - }, - ); - } + final currentUser = _ref.read(currentUserProvider); + if (currentUser == null) { + _logger.warning("[_handleBackup 3] No current user found. Skipping backup from background"); + return; + } - return hashFuture; - }); + _logger.info("[_handleBackup 4] Resume backup from background"); + if (Platform.isIOS) { + return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); + } - futures.add(localSyncFuture); - if (syncRemote) { - final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote(); - futures.add(remoteSyncFuture); + final canPing = await _ref.read(serverInfoServiceProvider).ping(); + if (!canPing) { + _logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background"); + return; + } + + 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; } - await Future.wait(futures); + await _ref.read(backgroundSyncProvider).syncRemote(); + if (_isCleanedUp) { + return; + } + + var hashFuture = _ref.read(backgroundSyncProvider).hashAssets(); + if (hashTimeout != null) { + hashFuture = hashFuture.timeout( + hashTimeout, + onTimeout: () { + // Consume cancellation errors as we want to continue processing + }, + ); + } + + await hashFuture; } } +/// 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 { +Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); final (isar, drift, logDB) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false); + 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 90720fdc76..8044b298d3 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; 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'; @@ -35,6 +36,7 @@ class HashService { 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( @@ -49,7 +51,7 @@ class HashService { final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id); if (assetsToHash.isNotEmpty) { - await _hashAssets(assetsToHash); + await _hashAssets(album, assetsToHash); } } @@ -60,7 +62,7 @@ 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 { + Future _hashAssets(LocalAlbum album, List assetsToHash) async { int bytesProcessed = 0; final toHash = <_AssetToPath>[]; @@ -72,6 +74,9 @@ class HashService { final file = await _storageRepository.getFileForAsset(asset.id); if (file == null) { + _log.warning( + "Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt} from album: ${album.name}", + ); continue; } @@ -79,17 +84,17 @@ class HashService { toHash.add(_AssetToPath(asset: asset, path: file.path)); if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) { - await _processBatch(toHash); + 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, List<_AssetToPath> toHash) async { if (toHash.isEmpty) { return; } @@ -114,7 +119,9 @@ class HashService { if (hash?.length == 20) { hashed.add(asset.copyWith(checksum: base64.encode(hash!))); } else { - _log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}"); + _log.warning( + "Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}", + ); } } 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 b136b11bab..d333af7481 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -1,29 +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(); @@ -53,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 @@ -286,7 +281,7 @@ extension on Iterable { (e) => LocalAlbum( id: e.id, name: e.name, - updatedAt: tryFromSecondsSinceEpoch(e.updatedAt!) ?? DateTime.now(), + updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(), assetCount: e.assetCount, ), ).toList(); @@ -301,8 +296,8 @@ extension on Iterable { name: e.name, checksum: null, type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, - createdAt: tryFromSecondsSinceEpoch(e.createdAt!) ?? DateTime.now(), - updatedAt: tryFromSecondsSinceEpoch(e.updatedAt!) ?? DateTime.now(), + createdAt: tryFromSecondsSinceEpoch(e.createdAt) ?? DateTime.now(), + updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(), 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 d21cb7ab09..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, 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..78750c9bd0 --- /dev/null +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -0,0 +1,108 @@ +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:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.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) { + return; + } + + final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId); + if (remoteAlbum == null) { + return; + } + + // get assets that are uploaded but not in the remote album + final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId); + + 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..6c8e444d50 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -23,54 +23,18 @@ 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); } } - 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 +47,10 @@ class SyncStreamService { await _processBatch(items); } + if (event.type == SyncEntityType.syncResetV1) { + reset(); + } + items.add(event); } @@ -103,6 +71,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 +129,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 +169,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/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index d8042c707c..ffbb020345 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'; @@ -23,6 +24,7 @@ class BackgroundSyncManager { Cancelable? _syncTask; Cancelable? _syncWebsocketTask; Cancelable? _deviceAlbumSyncTask; + Cancelable? _linkedAlbumSyncTask; Cancelable? _hashTask; BackgroundSyncManager({ @@ -52,6 +54,12 @@ class BackgroundSyncManager { _syncWebsocketTask?.cancel(); _syncWebsocketTask = null; + if (_linkedAlbumSyncTask != null) { + futures.add(_linkedAlbumSyncTask!.future); + } + _linkedAlbumSyncTask?.cancel(); + _linkedAlbumSyncTask = null; + try { await Future.wait(futures); } on CanceledError { @@ -92,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(() { @@ -114,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(() { @@ -134,7 +151,10 @@ class BackgroundSyncManager { onRemoteSyncStart?.call(); - _syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync()); + _syncTask = runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).sync(), + debugLabel: 'remote-sync', + ); return _syncTask! .whenComplete(() { onRemoteSyncComplete?.call(); @@ -155,8 +175,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..d4094f74cc --- /dev/null +++ b/mobile/lib/domain/utils/sync_linked_album.dart @@ -0,0 +1,12 @@ +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'; + +Future syncLinkedAlbumsIsolated(ProviderContainer ref) { + final user = Store.tryGet(StoreKey.currentUser); + if (user == null) { + 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/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_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 3130e41dbb..337a6d728d 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -20,7 +20,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { Set get primaryKey => {id}; } -extension LocalAssetEntityDataDomainEx on LocalAssetEntityData { +extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { LocalAsset toDto() => LocalAsset( id: id, name: name, diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index c3612cb8c6..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, @@ -81,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/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart index d98067d5fd..1e9f69147c 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)), @@ -137,22 +137,4 @@ class DriftBackupRepository extends DriftDatabaseRepository { 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..f04ed27779 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 => 10; @override MigrationStrategy get migration => MigrationStrategy( @@ -123,6 +148,14 @@ 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)); + }, ), ); @@ -138,7 +171,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..be6d53d5a8 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -3435,6 +3435,841 @@ 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, + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -3443,6 +4278,8 @@ 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, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -3481,6 +4318,16 @@ 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; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -3495,6 +4342,8 @@ 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, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -3504,5 +4353,7 @@ i1.OnUpgrade stepByStep({ from5To6: from5To6, from6To7: from6To7, from7To8: from7To8, + from8To9: from8To9, + from9To10: from9To10, ), ); diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 0c29768880..b97884c27f 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 @@ -136,7 +142,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 @@ -257,7 +263,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return Future.value(); } - if (_platform.isAndroid) { + if (CurrentPlatform.isAndroid) { return _deleteAssets(assetIds); } @@ -335,4 +341,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..05c8e06678 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,6 +1,8 @@ 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,6 +28,12 @@ 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) { @@ -69,4 +77,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/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 44a288787e..78b56e7436 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -113,6 +113,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .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( @@ -193,14 +202,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(); @@ -321,6 +329,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..01aa10c7ad 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -55,6 +55,12 @@ 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([]); 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..3969286d28 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,12 +102,12 @@ 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); @@ -130,6 +141,7 @@ class SyncApiRepository { } const _kResponseMap = { + SyncEntityType.authUserV1: SyncAuthUserV1.fromJson, SyncEntityType.userV1: SyncUserV1.fromJson, SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, SyncEntityType.partnerV1: SyncPartnerV1.fromJson, @@ -156,7 +168,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 +185,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..960a84435f 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,6 +32,65 @@ 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))); @@ -47,6 +109,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)); @@ -573,3 +636,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..86f68c397e 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.dateFmt(groupBy); + return TimeBucket(date: date, assetCount: row.assetCount); + }).watch(); } Future> _getMainBucketAssets(List userIds, {required int offset, required int count}) { @@ -595,7 +591,7 @@ extension on String { GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"), }; try { - return DateFormat(format).parse(this); + return DateFormat(format, 'en').parse(this); } catch (e) { throw FormatException("Invalid date format: $this", e); } 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 bf35077e1f..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 diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 207e522587..7d944c54ce 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -16,17 +16,15 @@ 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/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/backup/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/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; @@ -41,6 +39,7 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; import 'package:timezone/data/latest.dart'; import 'package:worker_manager/worker_manager.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; void main() async { ImmichWidgetsBinding(); @@ -71,9 +70,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"); } } @@ -128,23 +127,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; } @@ -202,18 +201,15 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve @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 if (Store.isBetaTimelineEnabled) { - ref.read(driftBackgroundUploadFgService).enableSyncService(); - if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) { - ref.read(backgroundServiceProvider).disableService(); - ref.read(driftBackgroundUploadFgService).enableUploadService(); - } + ref.read(backgroundServiceProvider).disableService(); + ref.read(backgroundWorkerFgServiceProvider).enable(); } else { + ref.read(backgroundWorkerFgServiceProvider).disable(); ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - ref.read(driftBackgroundUploadFgService).disableUploadService(); } }); @@ -244,7 +240,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 5140c62a0d..dc452d49d3 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -1,18 +1,23 @@ +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/presentation/widgets/backup/backup_toggle_button.widget.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.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/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; @RoutePage() @@ -24,6 +29,8 @@ class DriftBackupPage extends ConsumerStatefulWidget { } class _DriftBackupPageState extends ConsumerState { + Timer? _countPoller; + @override void initState() { super.initState(); @@ -32,24 +39,40 @@ class _DriftBackupPageState extends ConsumerState { return; } + if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) { + _startCountPolling(); + } + ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); } - Future startBackup() async { - final currentUser = ref.read(currentUserProvider); - if (currentUser == null) { - return; - } + void _startCountPolling() { + _countPoller?.cancel(); + _countPoller = Timer.periodic(const Duration(seconds: 5), (timer) async { + if (!mounted) { + timer.cancel(); + return; + } - await ref.read(backgroundSyncProvider).syncRemote(); - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); - await ref.read(driftBackgroundUploadFgService).enableUploadService(); - await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id); + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + timer.cancel(); + return; + } + + await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + }); } - Future stopBackup() async { - await ref.read(driftBackgroundUploadFgService).disableUploadService(); - await ref.read(driftBackupProvider.notifier).cancel(); + void _stopCountPolling() { + _countPoller?.cancel(); + _countPoller = null; + } + + @override + void dispose() { + _stopCountPolling(); + super.dispose(); } @override @@ -59,6 +82,26 @@ class _DriftBackupPageState extends ConsumerState { .where((album) => album.backupSelection == BackupSelection.selected) .toList(); + final backupNotifier = ref.read(driftBackupProvider.notifier); + final backgroundManager = ref.read(backgroundSyncProvider); + + Future startBackup() async { + final currentUser = Store.tryGet(StoreKey.currentUser); + if (currentUser == null) { + return; + } + + await backgroundManager.syncRemote(); + await backupNotifier.getBackupStatus(currentUser.id); + await backupNotifier.startBackup(currentUser.id); + _startCountPolling(); + } + + Future stopBackup() async { + await backupNotifier.cancel(); + _stopCountPolling(); + } + return Scaffold( appBar: AppBar( elevation: 0, 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..8cfdd2db95 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -1,12 +1,14 @@ 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/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/domain/services/sync_linked_album.service.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/user.provider.dart'; @@ -26,10 +28,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState _enableSyncUploadAlbum; late TextEditingController _searchController; late FocusNode _searchFocusNode; + Future? _handleLinkedAlbumFuture; @override void initState() { @@ -44,6 +46,36 @@ 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; + } + + // Restart backup if total count changed and backup is enabled + final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); + final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; + final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + + if (totalChanged && isBackupEnabled) { + await ref.read(driftBackupProvider.notifier).cancel(); + await ref.read(driftBackupProvider.notifier).startBackup(user.id); + } + } + @override void dispose() { _enableSyncUploadAlbum.dispose(); @@ -65,42 +97,12 @@ 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; - - final currentUser = ref.read(currentUserProvider); - if (currentUser == null) { - return; - } - - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.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 backupNotifier = ref.read(driftBackupProvider.notifier); - - backupNotifier.cancel().then((_) { - backupNotifier.startBackup(currentUser.id); - }); - } + canPop: false, + onPopInvokedWithResult: (didPop, _) async { + if (!didPop) { + await _handlePagePopped(); + Navigator.of(context).pop(); } }, child: Scaffold( @@ -139,103 +141,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/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 9bb2895907..2e78b79232 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,6 +15,7 @@ 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/background.service.dart'; @@ -41,49 +44,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)); - 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(driftBackgroundUploadFgService).disableUploadService(); - } - - 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(() { @@ -101,6 +68,54 @@ 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 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..f288464c42 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -4,7 +4,9 @@ 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 +23,7 @@ class SplashScreenPage extends StatefulHookConsumerWidget { class SplashScreenPageState extends ConsumerState { final log = Logger("SplashScreenPage"); + @override void initState() { super.initState(); @@ -47,11 +50,28 @@ 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) { + await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]); + await Future.wait([ + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + ]); + + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); + } + } } catch (e) { log.severe('Failed establishing connection to the server: $e'); } @@ -69,7 +89,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 +113,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 1ecb24d465..b60fe1ddc1 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -7,19 +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 { @@ -30,28 +26,6 @@ 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; @@ -136,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(); @@ -146,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 index 646eb63b76..af7c78fd4b 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -25,6 +25,57 @@ List wrapResponse({Object? result, PlatformException? error, bool empty 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 @@ -32,6 +83,9 @@ class _PigeonCodec extends StandardMessageCodec { 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); } @@ -40,6 +94,8 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { + case 129: + return BackgroundWorkerSettings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -59,9 +115,9 @@ class BackgroundWorkerFgHostApi { final String pigeonVar_messageChannelSuffix; - Future enableSyncWorker() async { + Future enable() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -82,15 +138,15 @@ class BackgroundWorkerFgHostApi { } } - Future enableUploadWorker(int callbackHandle) async { + Future configure(BackgroundWorkerSettings settings) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix'; + '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([callbackHandle]); + 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); @@ -105,9 +161,9 @@ class BackgroundWorkerFgHostApi { } } - Future disableUploadWorker() async { + Future disable() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -164,13 +220,57 @@ class BackgroundWorkerBgHostApi { 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 onLocalSync(int? maxSeconds); - Future onIosUpload(bool isRefresh, int? maxSeconds); Future onAndroidUpload(); @@ -183,35 +283,6 @@ abstract class BackgroundWorkerFlutterApi { String messageChannelSuffix = '', }) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$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.onLocalSync was null.', - ); - final List args = (message as List?)!; - final int? arg_maxSeconds = (args[0] as int?); - try { - await api.onLocalSync(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.onIosUpload$messageChannelSuffix', 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/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 60a296a22c..5ec946858d 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -15,6 +15,7 @@ class MainTimelinePage extends ConsumerWidget { 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/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_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..c899dad119 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), + withScrubber: 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/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/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_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 6d2f18f8f5..f57186d800 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -3,12 +3,14 @@ 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/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart'; @@ -29,7 +31,6 @@ 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 { @@ -52,10 +53,9 @@ 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(); @@ -85,7 +85,6 @@ class _AssetViewerState extends ConsumerState { PhotoViewControllerBase? viewController; StreamSubscription? reloadSubscription; - late Platform platform; late final int heroOffset; late PhotoViewControllerValue initialPhotoViewState; bool? hasDraggedDown; @@ -113,7 +112,6 @@ class _AssetViewerState extends ConsumerState { 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(_onAssetInit); @@ -129,6 +127,7 @@ class _AssetViewerState extends ConsumerState { reloadSubscription?.cancel(); _prevPreCacheStream?.removeListener(_dummyListener); _nextPreCacheStream?.removeListener(_dummyListener); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.dispose(); } @@ -596,6 +595,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); @@ -612,6 +612,15 @@ class _AssetViewerState extends ConsumerState { }); }); + // 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 @@ -626,7 +635,7 @@ class _AssetViewerState extends ConsumerState { gaplessPlayback: true, loadingBuilder: _placeholderBuilder, pageController: pageController, - scrollPhysics: platform.isIOS + scrollPhysics: CurrentPlatform.isIOS ? const FastScrollPhysics() // Use bouncing physics for iOS : const FastClampingScrollPhysics(), // Use heavy physics for Android itemCount: totalAssets, 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 e581e32df0..3111512823 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -62,7 +62,7 @@ class ViewerBottomBar extends ConsumerWidget { duration: Durations.short2, child: AnimatedSwitcher( duration: Durations.short4, - child: isSheetOpen || isReadonlyModeEnabled + child: isSheetOpen ? const SizedBox.shrink() : Theme( data: context.themeData.copyWith( @@ -72,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 ae55fb671b..7431290ad8 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -5,6 +5,7 @@ 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'; @@ -14,6 +15,7 @@ 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'; @@ -41,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, @@ -49,6 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget { isTrashEnabled: isTrashEnable, isInLockedView: isInLockedView, currentAlbum: currentAlbum, + advancedTroubleshooting: advancedTroubleshooting, source: ActionSource.viewer, ); @@ -122,6 +126,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); @@ -132,10 +140,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 @@ -143,7 +147,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(), 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 570df1afbb..05492f17e4 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 @@ -14,9 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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/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'; @@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { 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; 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..558df4e496 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'; @@ -43,7 +43,7 @@ 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.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..a162dbbfb2 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'; @@ -43,7 +43,7 @@ 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.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..73ebf60067 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,8 @@ 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/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'; @@ -21,6 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_ 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 +54,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; @@ -83,24 +87,27 @@ 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), const DeleteActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), 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..1dcc52f349 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'; @@ -16,22 +18,75 @@ 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/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 +101,17 @@ 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.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 dd87d2f228..ab5ead5ca5 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -96,7 +96,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide final operation = cachedOperation; if (operation != null) { - this.cachedOperation = null; + cachedOperation = null; operation.cancel(); } } diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 49af53f1eb..0c3b37a3b4 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -47,10 +47,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 +159,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), ], ); } @@ -191,21 +193,53 @@ class _Map extends StatelessWidget { } } -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/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/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index be1d0f0873..5974f3e0a7 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -3,12 +3,14 @@ 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:intl/intl.dart' hide TextDirection; /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged @@ -44,7 +46,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 +76,11 @@ 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; late AnimationController _thumbAnimationController; Timer? _fadeOutTimer; @@ -103,6 +107,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 +124,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(); } } @@ -138,6 +144,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 @@ -167,11 +177,15 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi } void _onDragStart(DragStartDetails _) { - ref.read(timelineStateProvider.notifier).setScrubbing(true); + if (_monthCount >= kMinMonthsToEnableScrubberSnap) { + ref.read(timelineStateProvider.notifier).setScrubbing(true); + } + setState(() { _isDragging = true; _labelAnimationController.forward(); _fadeOutTimer?.cancel(); + _lastLabel = null; }); } @@ -188,6 +202,20 @@ 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; + } + } + + if (_monthCount < kMinMonthsToEnableScrubberSnap) { + // 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); } } @@ -362,7 +390,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.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 125f8505a1..141621dae3 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -32,17 +32,17 @@ 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, }); final Widget? topSliverWidget; final double? topSliverWidgetHeight; - final bool? showStorageIndicator; + final bool showStorageIndicator; final Widget? appBar; final Widget? bottomSheet; final bool withStack; @@ -356,7 +356,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..b7f1b9ebb2 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'; @@ -19,7 +19,6 @@ 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 +32,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 +47,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 +83,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 +109,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 +124,103 @@ 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(); + + // 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 { + await Future.wait([ + _safeRun(backgroundManager.syncLocal(), "syncLocal"), + _safeRun(backgroundManager.syncRemote(), "syncRemote"), + ]); + + await Future.wait([ + _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { + _resumeBackup(); + }), + _resumeBackup(), + ]); + + 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 { + 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,9 +234,7 @@ class AppLifeCycleNotifier extends StateNotifier { try { LogService.I.flush(); - } catch (e) { - // Ignore flush errors during pause - } + } catch (_) {} } Future handleAppDetached() async { @@ -158,9 +243,7 @@ class AppLifeCycleNotifier extends StateNotifier { // Flush logs before closing database try { LogService.I.flush(); - } catch (e) { - // Ignore flush errors during shutdown - } + } catch (_) {} // Close Isar database safely try { @@ -168,9 +251,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 +260,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 6035e53e5d..03666466ff 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -2,11 +2,8 @@ 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/domain/services/background_worker.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -18,7 +15,6 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; -import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; @@ -35,8 +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; - -final driftBackgroundUploadFgService = Provider((ref) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi())); +import 'package:immich_mobile/utils/debug_print.dart'; final backupProvider = StateNotifierProvider((ref) { return BackupNotifier( @@ -290,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"); } /// @@ -432,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..1de7f75135 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -4,15 +4,15 @@ 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/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 { @@ -234,10 +234,6 @@ 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); - } - // Remove the completed task from the upload items if (state.uploadItems.containsKey(taskId)) { Future.delayed(const Duration(milliseconds: 1000), () { @@ -329,16 +325,16 @@ class DriftBackupNotifier extends StateNotifier { } Future cancel() async { - debugPrint("Canceling backup tasks..."); + dPrint(() => "Canceling backup tasks..."); state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true); 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: {}); } @@ -380,5 +376,5 @@ final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family< 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/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 65b4327b7a..03e2dfc6d5 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -1,3 +1,4 @@ +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'; @@ -6,6 +7,7 @@ import 'package:immich_mobile/models/download/livephotos_medatada.model.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'; @@ -115,6 +117,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 { diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart index 6469624c09..dec5c6905e 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -1,7 +1,14 @@ 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/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 nativeSyncApiProvider = Provider((_) => NativeSyncApi()); +final connectivityApiProvider = Provider((_) => ConnectivityApi()); + final thumbnailApi = ThumbnailApi(); 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/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/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/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/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 c8b06ae102..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()); @@ -31,7 +45,7 @@ class UploadRepository { return FileDownloader().enqueue(task); } - Future enqueueBackgroundAll(List tasks) { + Future> enqueueBackgroundAll(List tasks) { return FileDownloader().enqueueAll(tasks); } @@ -65,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..cdf384fcf8 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -76,7 +76,7 @@ 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'; @@ -86,6 +86,7 @@ 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 +333,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 +344,7 @@ 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]), // 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..981828acf1 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 { @@ -2629,6 +2650,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/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 9a25466609..7149408e8a 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -47,10 +47,12 @@ 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), + 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/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..0bce9366d2 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))); @@ -14,6 +14,15 @@ class ServerInfoService { const ServerInfoService(this._apiService); + Future ping() async { + try { + await _apiService.serverInfoApi.pingServer().timeout(const Duration(seconds: 5)); + return true; + } catch (e) { + return false; + } + } + Future getDiskInfo() async { try { final dto = await _apiService.serverInfoApi.getStorage(); @@ -21,7 +30,7 @@ class ServerInfoService { return ServerDiskInfo.fromDto(dto); } } catch (e) { - debugPrint("Error [getDiskInfo] ${e.toString()}"); + dPrint(() => "Error [getDiskInfo] ${e.toString()}"); } return null; } @@ -33,7 +42,7 @@ class ServerInfoService { return ServerVersion.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerVersion] ${e.toString()}"); + dPrint(() => "Error [getServerVersion] ${e.toString()}"); } return null; } @@ -45,7 +54,7 @@ class ServerInfoService { return ServerFeatures.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerFeatures] ${e.toString()}"); + dPrint(() => "Error [getServerFeatures] ${e.toString()}"); } return null; } @@ -57,7 +66,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 9c82e72ca6..9e9c81076b 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -3,7 +3,7 @@ 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'; @@ -19,7 +19,9 @@ 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:logging/logging.dart'; import 'package:path/path.dart' as p; +import 'package:immich_mobile/utils/debug_print.dart'; final uploadServiceProvider = Provider((ref) { final service = UploadService( @@ -51,6 +53,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,7 +81,7 @@ class UploadService { _taskProgressController.close(); } - Future enqueueTasks(List tasks) { + Future> enqueueTasks(List tasks) { return _uploadRepository.enqueueBackgroundAll(tasks); } @@ -138,7 +141,6 @@ class UploadService { } final batch = candidates.skip(i).take(batchSize).toList(); - List tasks = []; for (final asset in batch) { final task = await _getUploadTask(asset); @@ -156,9 +158,7 @@ class UploadService { } } - // Enqueue All does not work from the background on Android yet. This method is a temporary workaround - // that enqueues tasks one by one. - Future startBackupSerial(String userId) async { + Future startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async { await _storageRepository.clearCache(); shouldAbortQueuingTasks = false; @@ -168,14 +168,29 @@ class UploadService { return; } - for (final asset in candidates) { - if (shouldAbortQueuingTasks) { + const batchSize = 100; + for (int i = 0; i < candidates.length; i += batchSize) { + if (shouldAbortQueuingTasks || token.isCancelled) { break; } - final task = await _getUploadTask(asset); - if (task != null) { - await _uploadRepository.enqueueBackground(task); + 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); } } } @@ -238,10 +253,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) { @@ -282,6 +333,8 @@ class UploadService { return buildUploadTask( file, + createdAt: asset.createdAt, + modifiedAt: asset.updatedAt, originalFileName: originalFileName, deviceAssetId: asset.id, metadata: metadata, @@ -309,6 +362,8 @@ class UploadService { return buildUploadTask( file, + createdAt: asset.createdAt, + modifiedAt: asset.updatedAt, originalFileName: asset.name, deviceAssetId: asset.id, fields: fields, @@ -334,6 +389,8 @@ class UploadService { Future buildUploadTask( File file, { required String group, + required DateTime createdAt, + required DateTime modifiedAt, Map? fields, String? originalFileName, String? deviceAssetId, @@ -347,15 +404,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..4dfc0398bd 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'; @@ -15,7 +17,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act 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/upload_action_button.widget.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class ActionButtonContext { final BaseAsset asset; @@ -24,6 +25,7 @@ class ActionButtonContext { final bool isTrashEnabled; final bool isInLockedView; final RemoteAlbum? currentAlbum; + final bool advancedTroubleshooting; final ActionSource source; const ActionButtonContext({ @@ -33,11 +35,13 @@ class ActionButtonContext { required this.isTrashEnabled, required this.isInLockedView, required this.currentAlbum, + required this.advancedTroubleshooting, required this.source, }); } enum ActionButtonType { + advancedInfo, share, shareLink, archive, @@ -55,6 +59,7 @@ enum ActionButtonType { bool shouldShow(ActionButtonContext context) { return switch (this) { + ActionButtonType.advancedInfo => context.advancedTroubleshooting, ActionButtonType.share => true, ActionButtonType.shareLink => !context.isInLockedView && // @@ -115,6 +120,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), @@ -138,6 +144,7 @@ enum ActionButtonType { class ActionButtonBuilder { static const List _actionTypes = [ + ActionButtonType.advancedInfo, ActionButtonType.share, ActionButtonType.shareLink, ActionButtonType.likeActivity, 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 e7abc66040..c77ceaa62d 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -89,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/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 cca1498e0f..e8b7d410f4 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(); + 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.dispose(); - 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); + 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 { + ref.dispose(); - ref.dispose(); - } catch (error, stack) { - debugPrint("Error closing resources in isolate: $error, $stack"); - } finally { - ref.dispose(); - // Delay to ensure all resources are released - await Future.delayed(const Duration(seconds: 2)); - } - } + 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)); + } + } + return null; + }, + (error, stack) { + dPrint(() => "Error in isolate $debugLabel zone: $error, $stack"); + }, + ); return null; }); } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 9816986b93..f4f0564425 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,18 @@ 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/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 = 16; 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 +62,31 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.populateCache(); } + // Handle migration only for this version + // TODO: remove when old timeline is removed + final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); + if (version >= 15 && needBetaMigration == null) { + // Check both databases directly instead of relying on cache + + final isBeta = Store.tryGet(StoreKey.betaTimeline); + final isNewInstallation = await _isNewInstallation(db, drift); + + // For new installations, no migration needed + // For existing installations, only migrate if beta timeline is not enabled (null or false) + if (isNewInstallation || isBeta == true || (version > 15 && isBeta == null)) { + await Store.put(StoreKey.needBetaMigration, false); + await Store.put(StoreKey.betaTimeline, true); + } else { + await drift.reset(); + await Store.put(StoreKey.needBetaMigration, true); + } + } + + if (version < 16) { + await SyncStreamRepository(drift).reset(); + await Store.put(StoreKey.shouldResetSync, true); + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -78,6 +99,35 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } +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 +149,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 +169,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 +185,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 +212,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 +260,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 +278,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 +293,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 +304,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/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 b204058859..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'; @@ -259,7 +260,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { const AppBarProfileInfoBox(), buildStorageInformation(), const AppBarServerInfo(), - if (isReadonlyModeEnabled) buildReadonlyMessage(), + 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 a9c7a467c2..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 @@ -90,11 +90,11 @@ class AppBarProfileInfoBox extends HookConsumerWidget { minLeadingWidth: 50, leading: GestureDetector( onTap: pickUserProfileImage, - onDoubleTap: toggleReadonlyMode, + onLongPress: toggleReadonlyMode, child: Stack( clipBehavior: Clip.none, children: [ - buildUserProfileImage(), + AbsorbPointer(child: buildUserProfileImage()), if (!isReadonlyModeEnabled) Positioned( bottom: -5, diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 78fa607666..09c0518a23 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -97,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, ), ); }, @@ -157,7 +139,7 @@ class _ProfileIndicator extends ConsumerWidget { return InkWell( onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), - onDoubleTap: () => toggleReadonlyMode(), + onLongPress: () => toggleReadonlyMode(), borderRadius: const BorderRadius.all(Radius.circular(12)), child: Badge( label: Container( @@ -173,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)), ), ), ); @@ -296,7 +278,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/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index f9768d575e..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, + ), + ); + }, + ), + ); } } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index ab78536a92..7b5d31544a 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -10,9 +10,11 @@ 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'; @@ -161,6 +163,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 +192,7 @@ class LoginForm extends HookConsumerWidget { final isBeta = Store.isBetaTimelineEnabled; if (isBeta) { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - + handleSyncFlow(); context.replaceRoute(const TabShellRoute()); return; } @@ -276,6 +290,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/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 cd2fa93b85..7a107b47d8 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -14,6 +14,7 @@ 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'; @@ -91,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, @@ -101,12 +102,13 @@ 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(), - ), - // TODO: Remove this check when beta timeline goes stable + 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, 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..56506072c5 --- /dev/null +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -0,0 +1,354 @@ +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); + + 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(); + } + + 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..326d6c49d1 100644 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -1,5 +1,3 @@ -import 'dart:math' as math; - import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,50 +10,11 @@ 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); @@ -64,168 +23,49 @@ class _BetaTimelineListTileState extends ConsumerState wit 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 1a20e769ef..61854830d9 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -9,9 +9,11 @@ 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/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/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 d059aae656..239c62449f 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.140.1 +- API version: 1.142.1 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -107,7 +107,7 @@ Class | Method | HTTP request | Description *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 | @@ -126,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} | @@ -171,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} | @@ -416,8 +419,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) @@ -566,7 +571,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 a197f17fa7..e87c160d96 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -190,8 +190,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'; @@ -340,7 +342,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 e16ac2f535..063f9ea43b 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -729,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]. /// @@ -823,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: /// 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 3ea3b3c3e3..ae5fd9227b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -434,10 +434,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': @@ -734,8 +738,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/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_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 1a86b870e1..1b4ca91f3b 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -69,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 = [ @@ -118,6 +119,7 @@ class SyncEntityType { userMetadataDeleteV1, syncAckV1, syncResetV1, + syncCompleteV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -202,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/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 index eb1b7a2c5e..6f6c781de2 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -11,15 +11,20 @@ import 'package:pigeon/pigeon.dart'; dartPackageName: 'immich_mobile', ), ) +class BackgroundWorkerSettings { + final bool requiresCharging; + final int minimumDelaySeconds; + + const BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds}); +} + @HostApi() abstract class BackgroundWorkerFgHostApi { - void enableSyncWorker(); + void enable(); - // Enables the background upload service with the given callback handle - void enableUploadWorker(int callbackHandle); + void configure(BackgroundWorkerSettings settings); - // Disables the background upload service - void disableUploadWorker(); + void disable(); } @HostApi() @@ -27,14 +32,15 @@ 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 { - // Android & iOS: Called when the local sync is triggered - @async - void onLocalSync(int? maxSeconds); - // iOS Only: Called when the iOS background upload is triggered @async void onIosUpload(bool isRefresh, int? maxSeconds); 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/pubspec.lock b/mobile/pubspec.lock index 624ed1fe65..92e2a56172 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: @@ -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.3" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 48b49280e3..bd8817ce06 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.140.1+3011 +version: 1.142.1+3015 environment: sdk: '>=3.8.0 <4.0.0' - flutter: 3.32.8 + flutter: 3.35.3 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 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..76573e4997 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -11,6 +11,8 @@ 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; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -32,10 +34,14 @@ 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); 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]; } 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_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/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..497246e2a1 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -81,6 +81,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -110,6 +111,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -124,6 +126,7 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -141,6 +144,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -156,6 +160,7 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -171,6 +176,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -188,6 +194,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -203,6 +210,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -218,6 +226,7 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -233,6 +242,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -248,6 +258,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -265,6 +276,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -280,6 +292,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -295,6 +308,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -312,6 +326,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -327,6 +342,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -342,6 +358,7 @@ void main() { isTrashEnabled: true, isInLockedView: true, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -359,6 +376,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -374,6 +392,7 @@ void main() { isTrashEnabled: false, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -391,6 +410,7 @@ void main() { isTrashEnabled: false, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -406,6 +426,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -423,6 +444,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -440,6 +462,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -457,6 +480,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -472,6 +496,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -489,6 +514,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -506,6 +532,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -520,6 +547,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -537,6 +565,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -552,6 +581,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -567,6 +597,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -581,12 +612,45 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: 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, + 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, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.advancedInfo.shouldShow(context), isFalse); + }); + }); }); group('ActionButtonType.buildButton', () { @@ -602,6 +666,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); }); @@ -617,6 +682,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, source: ActionSource.timeline, ); final widget = buttonType.buildButton(contextWithAlbum); @@ -639,6 +705,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -658,6 +725,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: album, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -675,6 +743,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -693,6 +762,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); @@ -705,6 +775,7 @@ void main() { isTrashEnabled: true, isInLockedView: false, currentAlbum: null, + advancedTroubleshooting: false, source: ActionSource.timeline, ); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e2b2aa1905..0b0391326c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2504,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": [ { @@ -2566,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" } @@ -4994,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}": { @@ -5033,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", @@ -5069,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", @@ -5091,7 +5141,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdatePartnerDto" + "$ref": "#/components/schemas/PartnerUpdateDto" } } }, @@ -8848,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, @@ -8993,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, @@ -9790,7 +9858,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.140.1", + "version": "1.142.1", "contact": {} }, "tags": [], @@ -12853,6 +12921,18 @@ ], "type": "object" }, + "PartnerCreateDto": { + "properties": { + "sharedWithId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "sharedWithId" + ], + "type": "object" + }, "PartnerDirection": { "enum": [ "shared-by", @@ -12899,6 +12979,17 @@ ], "type": "object" }, + "PartnerUpdateDto": { + "properties": { + "inTimeline": { + "type": "boolean" + } + }, + "required": [ + "inTimeline" + ], + "type": "object" + }, "PeopleResponse": { "properties": { "enabled": { @@ -14571,6 +14662,10 @@ "query": { "type": "string" }, + "queryAssetId": { + "format": "uuid", + "type": "string" + }, "rating": { "maximum": 5, "minimum": -1, @@ -14638,9 +14733,6 @@ "type": "boolean" } }, - "required": [ - "query" - ], "type": "object" }, "SourceType": { @@ -15416,6 +15508,10 @@ ], "type": "object" }, + "SyncCompleteV1": { + "properties": {}, + "type": "object" + }, "SyncEntityType": { "enum": [ "AuthUserV1", @@ -15463,7 +15559,8 @@ "UserMetadataV1", "UserMetadataDeleteV1", "SyncAckV1", - "SyncResetV1" + "SyncResetV1", + "SyncCompleteV1" ], "type": "string" }, @@ -16990,6 +17087,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": { @@ -17005,6 +17110,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": { @@ -17235,17 +17348,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..e2228113dd 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index dbe0d8e57d..8f0d44dcbb 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.140.1", + "version": "1.142.1", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^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.19.0" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0f0357c32d..079dbda63b 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.140.1 + * 1.142.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -811,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 = { @@ -1014,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; @@ -1557,10 +1561,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") */ @@ -2360,7 +2368,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; @@ -3121,6 +3129,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. */ @@ -3133,9 +3156,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<{ @@ -3149,9 +3172,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; @@ -3159,7 +3182,7 @@ export function updatePartner({ id, updatePartnerDto }: { }>(`/partners/${encodeURIComponent(id)}`, oazapfts.json({ ...opts, method: "PUT", - body: updatePartnerDto + body: partnerUpdateDto }))); } /** @@ -4274,7 +4297,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; @@ -4286,6 +4309,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) { @@ -4304,6 +4328,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers timeBucket, userId, visibility, + withCoordinates, withPartners, withStacked }))}`, { @@ -4313,7 +4338,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; @@ -4324,6 +4349,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) { @@ -4341,6 +4367,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per tagId, userId, visibility, + withCoordinates, withPartners, withStacked }))}`, { @@ -4921,7 +4948,8 @@ export enum SyncEntityType { UserMetadataV1 = "UserMetadataV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1", SyncAckV1 = "SyncAckV1", - SyncResetV1 = "SyncResetV1" + SyncResetV1 = "SyncResetV1", + SyncCompleteV1 = "SyncCompleteV1" } export enum SyncRequestType { AlbumsV1 = "AlbumsV1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa24e5b31f..46b9ac746a 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,9 +41,6 @@ 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 @@ -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.4 '@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.4)(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 @@ -112,16 +109,16 @@ importers: version: 8.39.1(eslint@9.33.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.4)(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.4)(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.4)(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.4)(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.0(@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.0(@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.0(@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,16 @@ 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.0(@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) + version: 3.6.0(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@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) 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: 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.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.0(@types/react@19.1.13)(react@18.3.1))(@types/react@19.1.13)(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) lunr: specifier: ^2.3.9 version: 2.3.9 @@ -172,7 +163,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,9 +195,6 @@ importers: e2e: devDependencies: - '@eslint/eslintrc': - specifier: ^3.1.0 - version: 3.3.1 '@eslint/js': specifier: ^9.8.0 version: 9.33.0 @@ -226,8 +214,8 @@ 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.4 '@types/oidc-provider': specifier: ^9.0.0 version: 9.1.2 @@ -240,9 +228,6 @@ 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) @@ -283,8 +268,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 @@ -302,7 +287,7 @@ importers: 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.4)(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 +296,8 @@ importers: version: 1.0.4 devDependencies: '@types/node': - specifier: ^22.17.1 - version: 22.17.2 + specifier: ^22.18.1 + version: 22.18.4 typescript: specifier: ^5.3.3 version: 5.9.2 @@ -328,9 +313,6 @@ importers: '@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,9 +331,6 @@ 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) @@ -527,8 +506,8 @@ 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 @@ -541,9 +520,6 @@ 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) @@ -554,15 +530,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 '@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.3(@swc/helpers@0.5.17))(@types/node@22.18.4) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.7(chokidar@4.0.3)(typescript@5.9.2) @@ -572,12 +545,6 @@ importers: '@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 '@types/archiver': specifier: ^6.0.0 version: 6.0.3 @@ -618,11 +585,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.4 '@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,7 +598,7 @@ 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 @@ -649,10 +616,7 @@ importers: version: 13.15.2 '@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.4)(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) @@ -671,9 +635,6 @@ importers: 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 @@ -686,12 +647,6 @@ 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 @@ -704,9 +659,6 @@ 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 @@ -715,16 +667,13 @@ importers: version: 8.39.1(eslint@9.33.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.5(@swc/core@1.13.3(@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.4)(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.4)(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) web: dependencies: @@ -735,8 +684,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.28.1 + version: 0.28.1(@internationalized/date@3.8.2)(svelte@5.35.5) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -809,6 +758,9 @@ importers: 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 @@ -831,42 +783,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 '@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.33.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.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@types/node@24.3.0)(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.1.2(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(rollup@4.50.1)(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(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.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@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': 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)) + version: 6.1.2(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(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.12(vite@7.1.5(@types/node@24.3.0)(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 '@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.35.5)(vite@7.1.5(@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/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.0) @@ -891,9 +840,6 @@ importers: '@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) dotenv: specifier: ^17.0.0 version: 17.2.1 @@ -926,7 +872,7 @@ importers: 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) @@ -935,31 +881,28 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@5.35.5) 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 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.35.5)(typescript@5.9.2) svelte-eslint-parser: specifier: ^1.2.0 version: 1.3.1(svelte@5.35.5) tailwindcss: specifier: ^4.1.7 version: 4.1.12 - tslib: - specifier: ^2.6.2 - version: 2.8.1 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.39.1(eslint@9.33.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.3.0)(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) @@ -1073,6 +1016,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 +1153,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,11 +1244,6 @@ 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==} engines: {node: '>=6.0.0'} @@ -1630,30 +1689,18 @@ 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==} - 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==} engines: {node: '>=6.9.0'} @@ -1679,8 +1726,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 +1737,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 +2171,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==} @@ -2443,10 +2490,6 @@ packages: 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} @@ -2467,10 +2510,6 @@ packages: 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} @@ -2478,14 +2517,14 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} + '@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'} + '@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'} - '@fig/complete-commander@3.2.0': resolution: {integrity: sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==} peerDependencies: @@ -2494,8 +2533,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==} @@ -2556,124 +2595,130 @@ packages: 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.28.1': + resolution: {integrity: sha512-FAQBPsbPaLtTYsEH+/wQxPbUI19ZPuOEScSHgSqteI601qVZNQcfU7YuinKqK94iva2RNhvUgrNGHweIBentZg==} peerDependencies: svelte: ^5.0.0 @@ -2853,10 +2898,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 +2905,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==} @@ -3088,12 +3119,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: @@ -3213,13 +3238,6 @@ packages: 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==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3304,102 +3322,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-amqplib@0.50.0': - resolution: {integrity: sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==} - 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==} - 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==} - 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==} - 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==} - 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==} - 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==} - 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==} - 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} @@ -3412,138 +3334,18 @@ packages: 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} @@ -3568,12 +3370,6 @@ packages: 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==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3590,36 +3386,6 @@ 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==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3900,103 +3666,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 +3806,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 +3983,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==} @@ -4341,12 +4277,6 @@ packages: 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'} @@ -4410,9 +4340,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 +4352,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==} @@ -4595,9 +4519,6 @@ packages: '@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 +4534,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,9 +4552,6 @@ 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==} @@ -4652,24 +4567,18 @@ packages: '@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.17.2': - resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + '@types/node@22.18.4': + resolution: {integrity: sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==} '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} - '@types/nodemailer@6.4.17': - resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} + '@types/nodemailer@7.0.1': + resolution: {integrity: sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==} '@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/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} @@ -4712,11 +4621,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==} @@ -4763,9 +4669,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,6 +4681,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/validator@13.15.2': resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} @@ -4808,45 +4714,22 @@ packages: 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==} 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==} - 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==} 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==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4854,33 +4737,16 @@ packages: 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==} - 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==} 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==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4888,10 +4754,6 @@ packages: 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==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5151,10 +5013,6 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 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 +5024,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==} @@ -5310,9 +5164,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 +5221,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 +5249,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 +5270,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} @@ -5535,9 +5381,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==} @@ -5643,9 +5486,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'} @@ -5916,9 +5756,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==} @@ -6137,9 +5974,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==} @@ -6177,9 +6011,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 +6025,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'} @@ -6417,10 +6240,6 @@ 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==} engines: {node: '>=12'} @@ -6444,9 +6263,6 @@ 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==} @@ -6494,10 +6310,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'} @@ -6612,6 +6424,7 @@ packages: eslint-p@0.25.0: resolution: {integrity: sha512-e7oYgXN/tgtoaR3tZ0R2dKyPJtf5J41hYKsgpsBtwpi0t2Cxjf3l8G2QwrXCDwQTFVXW1hmD55hAqQZxiId1XA==} 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: @@ -6775,9 +6588,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==} @@ -6868,6 +6678,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==} @@ -6994,10 +6808,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'} @@ -7074,14 +6884,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'} @@ -7141,9 +6943,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==} @@ -7205,10 +7004,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'} @@ -7878,9 +7673,6 @@ 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==} @@ -7926,9 +7718,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==} @@ -8694,9 +8483,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==} @@ -8782,10 +8568,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} @@ -8822,11 +8604,6 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-gyp@11.2.0: - resolution: {integrity: sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==} - 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} @@ -8894,8 +8671,8 @@ 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==} @@ -10264,11 +10041,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 +10057,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 @@ -10443,10 +10215,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 +10226,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: @@ -10529,6 +10297,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==} @@ -10642,10 +10414,6 @@ packages: resolution: {integrity: sha512-bZydXEXhaNDQBr8xYHC3a8thwcaMuTBp0CkKGjwGYDsIB26tnlWeWPwJtSQ0TEwiJcz9iJJON5mFPkx7XroHcg==} 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'} @@ -10766,6 +10534,9 @@ packages: 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==} engines: {node: '>=18'} @@ -10919,8 +10690,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' @@ -11055,8 +10826,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: @@ -11216,65 +10987,6 @@ 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==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -11319,9 +11031,6 @@ 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==} @@ -11555,8 +11264,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 +11433,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'} @@ -12081,8 +11780,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 +11794,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.4)(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.4) ansi-colors: 4.1.3 symbol-observable: 4.0.0 yargs-parser: 21.1.1 @@ -12120,12 +11819,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,14 +12223,14 @@ 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/template': 7.27.2 - '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -12154,14 +12239,6 @@ snapshots: 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 @@ -12190,7 +12267,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.3 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -12217,14 +12294,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -12234,7 +12311,7 @@ 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.3 transitivePeerDependencies: - supports-color @@ -12249,7 +12326,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.3 transitivePeerDependencies: - supports-color @@ -12258,13 +12335,13 @@ 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.3 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -12278,7 +12355,7 @@ snapshots: '@babel/helper-wrap-function@7.27.1': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -12286,11 +12363,7 @@ snapshots: '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.7 - - '@babel/parser@7.27.7': - dependencies: - '@babel/types': 7.27.7 + '@babel/types': 7.28.2 '@babel/parser@7.28.3': dependencies: @@ -12300,7 +12373,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -12327,7 +12400,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -12376,7 +12449,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.3 transitivePeerDependencies: - supports-color @@ -12422,7 +12495,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.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12437,7 +12510,7 @@ snapshots: dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -12486,7 +12559,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.3 transitivePeerDependencies: - supports-color @@ -12532,7 +12605,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.3 transitivePeerDependencies: - supports-color @@ -12572,7 +12645,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.3 transitivePeerDependencies: - supports-color @@ -12818,7 +12891,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 @@ -12857,9 +12930,7 @@ 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: @@ -12867,18 +12938,6 @@ snapshots: '@babel/parser': 7.28.3 '@babel/types': 7.28.2 - '@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': dependencies: '@babel/code-frame': 7.27.1 @@ -12891,11 +12950,6 @@ snapshots: 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': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -12915,16 +12969,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 +13002,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 +13011,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 +13020,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 +13050,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 +13066,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 +13149,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 +13170,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 +13198,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 +13229,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 +13246,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.3 '@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 +13278,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 +13312,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.0(@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 +13321,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.0(@types/react@19.1.13)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -13282,7 +13336,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 +13346,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 +13355,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 +13398,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 +13414,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 +13429,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 +13444,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.0(@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.0(@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.0(@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.0(@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.0(@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 +13466,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 +13486,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.0(@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.0(@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.0(@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 +13507,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 +13527,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.0(@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.0(@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 +13538,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 +13558,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.0(@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.0(@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 +13586,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.0(@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.0(@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 +13615,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.0(@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.0(@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 +13642,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.0(@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.0(@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 +13670,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.0(@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.0(@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 +13697,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.0(@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.0(@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 +13729,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.0(@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.0(@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 +13740,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 +13760,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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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 +13803,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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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.0(@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 +13855,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.0(@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.0(@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 +13880,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.0(@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.0(@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.0(@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.0(@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 +13933,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 +13991,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 +14004,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 +14017,7 @@ snapshots: - uglify-js - webpack-cli - '@emnapi/runtime@1.4.5': + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 optional: true @@ -14141,10 +14195,6 @@ snapshots: 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 @@ -14169,11 +14219,6 @@ snapshots: '@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 @@ -14181,9 +14226,9 @@ snapshots: '@exodus/schemasafe@1.3.0': {} - '@faker-js/faker@5.5.3': {} + '@faker-js/faker@10.0.0': {} - '@faker-js/faker@9.9.0': {} + '@faker-js/faker@5.5.3': {} '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: @@ -14194,7 +14239,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 +14250,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': @@ -14264,119 +14309,125 @@ snapshots: '@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.28.1(@internationalized/date@3.8.2)(svelte@5.35.5)': dependencies: '@mdi/js': 7.4.47 - 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.35.5) + simple-icons: 15.15.0 svelte: 5.35.5 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.12) tailwindcss: 4.1.12 transitivePeerDependencies: - '@internationalized/date' - '@inquirer/checkbox@4.2.1(@types/node@22.13.14)': + '@inquirer/checkbox@4.2.1(@types/node@22.18.4)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.4) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.4) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/confirm@5.1.15(@types/node@22.13.14)': + '@inquirer/confirm@5.1.15(@types/node@22.18.4)': 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.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/core@10.1.15(@types/node@22.13.14)': + '@inquirer/core@10.1.15(@types/node@22.18.4)': 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.4) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -14384,115 +14435,115 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/editor@4.2.17(@types/node@22.13.14)': + '@inquirer/editor@4.2.17(@types/node@22.18.4)': 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.4) + '@inquirer/external-editor': 1.0.1(@types/node@22.18.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/expand@4.0.17(@types/node@22.13.14)': + '@inquirer/expand@4.0.17(@types/node@22.18.4)': 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.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/external-editor@1.0.1(@types/node@22.13.14)': + '@inquirer/external-editor@1.0.1(@types/node@22.18.4)': dependencies: chardet: 2.1.0 iconv-lite: 0.6.3 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 '@inquirer/figures@1.0.13': {} - '@inquirer/input@4.2.1(@types/node@22.13.14)': + '@inquirer/input@4.2.1(@types/node@22.18.4)': 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.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/number@3.0.17(@types/node@22.13.14)': + '@inquirer/number@3.0.17(@types/node@22.18.4)': 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.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/password@4.0.17(@types/node@22.13.14)': + '@inquirer/password@4.0.17(@types/node@22.18.4)': 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.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) ansi-escapes: 4.3.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/prompts@7.3.2(@types/node@22.13.14)': + '@inquirer/prompts@7.3.2(@types/node@22.18.4)': 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.4) + '@inquirer/confirm': 5.1.15(@types/node@22.18.4) + '@inquirer/editor': 4.2.17(@types/node@22.18.4) + '@inquirer/expand': 4.0.17(@types/node@22.18.4) + '@inquirer/input': 4.2.1(@types/node@22.18.4) + '@inquirer/number': 3.0.17(@types/node@22.18.4) + '@inquirer/password': 4.0.17(@types/node@22.18.4) + '@inquirer/rawlist': 4.1.5(@types/node@22.18.4) + '@inquirer/search': 3.1.0(@types/node@22.18.4) + '@inquirer/select': 4.3.1(@types/node@22.18.4) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/prompts@7.8.0(@types/node@22.13.14)': + '@inquirer/prompts@7.8.0(@types/node@22.18.4)': 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.4) + '@inquirer/confirm': 5.1.15(@types/node@22.18.4) + '@inquirer/editor': 4.2.17(@types/node@22.18.4) + '@inquirer/expand': 4.0.17(@types/node@22.18.4) + '@inquirer/input': 4.2.1(@types/node@22.18.4) + '@inquirer/number': 3.0.17(@types/node@22.18.4) + '@inquirer/password': 4.0.17(@types/node@22.18.4) + '@inquirer/rawlist': 4.1.5(@types/node@22.18.4) + '@inquirer/search': 3.1.0(@types/node@22.18.4) + '@inquirer/select': 4.3.1(@types/node@22.18.4) optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/rawlist@4.1.5(@types/node@22.13.14)': + '@inquirer/rawlist@4.1.5(@types/node@22.18.4)': 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.4) + '@inquirer/type': 3.0.8(@types/node@22.18.4) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/search@3.1.0(@types/node@22.13.14)': + '@inquirer/search@3.1.0(@types/node@22.18.4)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.4) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.4) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/select@4.3.1(@types/node@22.13.14)': + '@inquirer/select@4.3.1(@types/node@22.18.4)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.13.14) + '@inquirer/core': 10.1.15(@types/node@22.18.4) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.13.14) + '@inquirer/type': 3.0.8(@types/node@22.18.4) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 - '@inquirer/type@3.0.8(@types/node@22.13.14)': + '@inquirer/type@3.0.8(@types/node@22.18.4)': optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 22.18.4 '@internationalized/date@3.8.2': dependencies: @@ -14530,7 +14581,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.4 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -14539,12 +14590,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 +14597,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 @@ -14588,12 +14624,12 @@ snapshots: 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.33.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.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) + browserslist: 4.25.3 transitivePeerDependencies: - eslint - supports-color @@ -14735,10 +14771,10 @@ snapshots: - acorn - supports-color - '@mdx-js/react@3.1.0(@types/react@19.1.10)(react@18.3.1)': + '@mdx-js/react@3.1.0(@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': {} @@ -14788,12 +14824,12 @@ snapshots: bullmq: 5.57.0 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.3(@swc/helpers@0.5.17))(@types/node@22.18.4)': 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.4)(chokidar@4.0.3) + '@inquirer/prompts': 7.8.0(@types/node@22.18.4) '@nestjs/schematics': 11.0.7(chokidar@4.0.3)(typescript@5.8.3) ansis: 4.1.0 chokidar: 4.0.3 @@ -14847,12 +14883,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) @@ -14988,62 +15018,6 @@ snapshots: '@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)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15163,140 +15137,6 @@ snapshots: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-amqplib@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-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 @@ -15316,82 +15156,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-kafkajs@0.13.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-knex@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-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/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 @@ -15400,23 +15164,6 @@ snapshots: 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 @@ -15429,81 +15176,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pino@0.50.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 @@ -15538,10 +15210,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 - '@opentelemetry/propagation-utils@0.31.3(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15554,45 +15222,6 @@ snapshots: '@opentelemetry/redis-common@0.38.0': {} - '@opentelemetry/resource-detector-alibaba-cloud@0.31.3(@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-aws@2.3.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-azure@0.10.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-container@0.7.3(@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 @@ -15859,72 +15488,75 @@ snapshots: 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.2.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 +15582,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 +15596,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,37 +15879,35 @@ 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.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@types/node@24.3.0)(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.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@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.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.1.2(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)))(rollup@4.50.1)(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(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)) + '@sveltejs/vite-plugin-svelte': 6.1.2(svelte@5.35.5)(vite@7.1.5(@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 + sharp: 0.34.3 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) + vite: 7.1.5(@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.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.27.1(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@types/node@24.3.0)(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.1.2(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(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 @@ -16018,27 +15920,27 @@ snapshots: 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) + vite: 7.1.5(@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.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.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@types/node@24.3.0)(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)) + '@sveltejs/vite-plugin-svelte': 6.1.2(svelte@5.35.5)(vite@7.1.5(@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) + vite: 7.1.5(@types/node@24.3.0)(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.1.2(svelte@5.35.5)(vite@7.1.5(@types/node@24.3.0)(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)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.1.2(svelte@5.35.5)(vite@7.1.5(@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.5(@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 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)) + vite: 7.1.5(@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.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) transitivePeerDependencies: - supports-color @@ -16260,31 +16162,17 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 - '@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.12(vite@7.1.5(@types/node@24.3.0)(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 + vite: 7.1.5(@types/node@24.3.0)(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 @@ -16301,12 +16189,12 @@ 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.35.5)(vite@7.1.5(@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))': dependencies: '@testing-library/dom': 10.4.0 svelte: 5.35.5 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.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/user-event@14.6.1(@testing-library/dom@10.4.0)': @@ -16349,7 +16237,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/archiver@6.0.3': dependencies: @@ -16359,27 +16247,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.4 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@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 +16279,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.3 - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@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.4 '@types/connect@3.4.38': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/content-disposition@0.5.9': {} @@ -16428,11 +16310,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.4 '@types/cors@2.8.19': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/debug@4.1.12': dependencies: @@ -16442,13 +16324,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@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.4 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -16471,14 +16353,14 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@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.4 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 @@ -16504,7 +16386,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.27': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/geojson-vt@3.2.5': dependencies: @@ -16528,7 +16410,7 @@ snapshots: '@types/hoist-non-react-statics@3.3.6': dependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.13 hoist-non-react-statics: 3.3.2 '@types/html-minifier-terser@6.1.0': {} @@ -16541,7 +16423,7 @@ snapshots: '@types/http-proxy@1.17.16': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/inquirer@8.2.11': dependencies: @@ -16579,7 +16461,7 @@ 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.4 '@types/leaflet@1.9.19': dependencies: @@ -16587,9 +16469,7 @@ snapshots: '@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 +16483,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 +16493,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/ms@2.1.0': {} @@ -16625,18 +16501,14 @@ 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': 22.18.4 + form-data: 4.0.4 '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/node@17.0.45': {} @@ -16648,11 +16520,7 @@ 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.4': dependencies: undici-types: 6.21.0 @@ -16661,35 +16529,34 @@ snapshots: undici-types: 7.10.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.4 + transitivePeerDependencies: + - aws-crt '@types/oidc-provider@9.1.2': 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.4 '@types/parse5@5.0.3': {} '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.15.4 + '@types/pg': 8.15.5 '@types/pg@8.15.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 pg-protocol: 1.10.3 pg-types: 2.2.0 '@types/pg@8.15.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -16697,13 +16564,13 @@ snapshots: '@types/pngjs@6.0.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/qs@6.14.0': {} @@ -16712,38 +16579,34 @@ snapshots: '@types/react-redux@7.1.34': dependencies: '@types/hoist-non-react-statics': 3.3.6 - '@types/react': 19.1.10 + '@types/react': 19.1.13 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.4 '@types/retry@0.12.0': {} @@ -16753,14 +16616,14 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/semver@7.7.0': {} '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/serve-index@1.9.4': dependencies: @@ -16769,20 +16632,20 @@ snapshots: '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/send': 0.17.5 '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/ssh2-streams@0.1.12': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/ssh2@0.5.52': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/ssh2-streams': 0.1.12 '@types/ssh2@1.15.5': @@ -16793,8 +16656,8 @@ snapshots: 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.4 + form-data: 4.0.4 '@types/supercluster@7.1.3': dependencies: @@ -16805,13 +16668,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.4 '@types/ua-parser-js@0.7.39': {} @@ -16819,13 +16678,15 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@9.0.8': {} + '@types/validator@13.15.2': {} '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 '@types/yargs-parser@21.0.3': {} @@ -16833,23 +16694,6 @@ 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)': - 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 @@ -16867,18 +16711,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': - 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 @@ -16891,24 +16723,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.35.0(typescript@5.8.3)': - 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) @@ -16918,51 +16732,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.35.0': - dependencies: - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.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)': dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.35.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': - 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 @@ -16975,42 +16753,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.35.0': {} - '@typescript-eslint/types@8.39.1': {} - '@typescript-eslint/typescript-estree@8.35.0(typescript@5.8.3)': - 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) @@ -17027,28 +16771,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.0(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.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)) @@ -17060,11 +16782,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.35.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 @@ -17072,7 +16789,7 @@ snapshots: '@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.4)(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: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17087,11 +16804,11 @@ snapshots: 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.4)(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@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@22.18.4)(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 @@ -17106,7 +16823,7 @@ snapshots: 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) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.4)(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 +16854,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.4)(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@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.4)(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.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@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.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -17427,8 +17136,6 @@ snapshots: ansi-styles@6.2.1: {} - ansis@3.17.0: {} - ansis@4.1.0: {} any-promise@1.3.0: {} @@ -17438,8 +17145,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 +17210,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 @@ -17533,8 +17238,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 +17250,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 +17274,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 +17291,6 @@ snapshots: balanced-match@1.0.2: {} - bare-events@2.5.4: - optional: true - bare-events@2.6.1: optional: true @@ -17635,14 +17337,12 @@ 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.35.5): 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) @@ -17694,6 +17394,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.12.1: {} + boxen@6.2.1: dependencies: ansi-align: 3.0.1 @@ -17729,13 +17431,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 @@ -17856,14 +17551,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 +17566,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: {} @@ -17977,8 +17671,6 @@ snapshots: libphonenumber-js: 1.12.9 validator: 13.15.15 - classnames@2.5.1: {} - clean-css@5.3.3: dependencies: source-map: 0.6.1 @@ -18220,7 +17912,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 +17920,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: {} @@ -18317,7 +18005,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 +18016,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 +18026,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 +18176,6 @@ snapshots: whatwg-url: 14.2.0 optional: true - dayjs@1.11.13: {} - debounce@1.2.1: {} debounce@2.2.0: {} @@ -18508,10 +18194,7 @@ snapshots: 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 +18203,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: {} @@ -18648,9 +18330,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.0(@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.0(@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,10 +18350,10 @@ 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): + docusaurus-plugin-openapi@0.7.6(@mdx-js/react@3.1.0(@types/react@19.1.13)(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/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@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) '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -18685,7 +18367,7 @@ snapshots: postman-collection: 4.5.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -18708,12 +18390,12 @@ snapshots: 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): + 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.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.0(@types/react@19.1.13)(react@18.3.1))(@types/react@19.1.13)(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/preset-classic': 3.8.1(@algolia/client-search@5.29.0)(@mdx-js/react@3.1.0(@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-plugin-openapi: 0.7.6(@mdx-js/react@3.1.0(@types/react@19.1.13)(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) + docusaurus-theme-openapi: 0.7.6(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@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))(@types/react@19.1.13)(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: @@ -18742,16 +18424,16 @@ snapshots: - 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): + docusaurus-theme-openapi@0.7.6(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@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))(@types/react@19.1.13)(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) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@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) + '@mdx-js/react': 3.1.0(@types/react@19.1.13)(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) + docusaurus-plugin-openapi: 0.7.6(@mdx-js/react@3.1.0(@types/react@19.1.13)(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 @@ -18767,7 +18449,7 @@ snapshots: redux-devtools-extension: 2.13.9(redux@4.2.1) refractor: 4.9.0 striptags: 3.2.0 - webpack: 5.99.9 + webpack: 5.100.2 transitivePeerDependencies: - '@docusaurus/faster' - '@docusaurus/plugin-content-docs' @@ -18849,8 +18531,6 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv@16.6.1: {} - dotenv@17.2.1: {} dunder-proto@1.0.1: @@ -18869,8 +18549,6 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.177: {} - electron-to-chromium@1.5.207: {} emoji-regex@10.4.0: {} @@ -18915,7 +18593,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 22.17.2 + '@types/node': 22.18.4 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -18928,11 +18606,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 @@ -19101,8 +18774,8 @@ snapshots: dependencies: '@mdn/browser-compat-data': 5.7.6 ast-metadata-inferer: 0.8.1 - browserslist: 4.25.1 - caniuse-lite: 1.0.30001726 + browserslist: 4.25.3 + caniuse-lite: 1.0.30001735 eslint: 9.33.0(jiti@2.5.1) find-up: 5.0.0 globals: 15.15.0 @@ -19141,7 +18814,7 @@ snapshots: 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/plugin-kit': 0.3.5 change-case: 5.4.4 ci-info: 4.3.0 clean-regexp: 1.0.0 @@ -19339,7 +19012,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 require-like: 0.1.2 event-emitter@0.3.5: @@ -19349,8 +19022,6 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter2@6.4.9: {} - eventemitter3@4.0.7: {} events@3.3.0: {} @@ -19512,6 +19183,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 +19217,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: @@ -19654,14 +19329,6 @@ snapshots: 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 @@ -19706,7 +19373,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 +19408,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: {} @@ -19816,8 +19463,6 @@ snapshots: github-slugger@1.5.0: {} - gl-matrix@3.4.3: {} - gl-matrix@3.4.4: {} glob-parent@5.1.2: @@ -19890,8 +19535,6 @@ snapshots: globrex@0.1.2: {} - google-logging-utils@0.0.2: {} - gopd@1.2.0: {} got@12.6.1: @@ -20130,7 +19773,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 +19842,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 +19850,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: @@ -20565,7 +20208,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.30 debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: @@ -20591,7 +20234,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.17.2 + '@types/node': 22.18.4 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -20599,13 +20242,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.4 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.4 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -20657,7 +20300,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 +20329,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 +20359,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,10 +20384,6 @@ 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: {} @@ -20793,12 +20432,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 @@ -21035,12 +20668,12 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@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.3 + '@babel/types': 7.28.2 source-map-js: 1.2.1 make-dir@3.1.0: @@ -21108,7 +20741,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 @@ -21689,7 +21322,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 +21331,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: {} @@ -21839,8 +21473,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.22.2: {} - nan@2.23.0: optional: true @@ -21917,8 +21549,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: @@ -21956,21 +21586,6 @@ snapshots: node-gyp-build@4.8.4: {} - node-gyp@11.2.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-gyp@11.3.0: dependencies: env-paths: 2.2.1 @@ -21981,7 +21596,7 @@ 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 @@ -22033,13 +21648,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: @@ -22498,7 +22113,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) @@ -22627,7 +22242,7 @@ snapshots: 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 +22263,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 +22460,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) @@ -23002,11 +22617,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 @@ -23098,7 +22708,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.4 long: 5.3.2 protocol-buffers-schema@3.6.0: {} @@ -23182,11 +22792,11 @@ snapshots: iconv-lite: 0.6.3 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: @@ -23240,11 +22850,11 @@ 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 + webpack: 5.100.2 react-magic-dropzone@1.0.1: {} @@ -23254,7 +22864,7 @@ snapshots: react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@types/react-redux': 7.1.34 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -23266,13 +22876,13 @@ snapshots: 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 +22893,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 @@ -23393,7 +23003,7 @@ snapshots: redux@4.2.1: dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 reflect-metadata@0.2.2: {} @@ -23602,46 +23212,42 @@ 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: @@ -23859,11 +23465,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 +23480,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 + node-addon-api: 8.5.0 + node-gyp: 11.3.0 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 @@ -23985,13 +23587,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: @@ -24144,8 +23750,6 @@ snapshots: argparse: 2.0.1 nearley: 2.20.1 - sql-highlight@6.1.0: {} - srcset@4.0.0: {} ssh-remote-port-forward@1.0.4: @@ -24190,7 +23794,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: @@ -24261,6 +23865,8 @@ snapshots: striptags@3.2.0: {} + strnum@2.1.1: {} + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -24344,7 +23950,7 @@ 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.35.5)(typescript@5.9.2): dependencies: '@jridgewell/trace-mapping': 0.3.30 chokidar: 4.0.3 @@ -24352,7 +23958,7 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 svelte: 5.35.5 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - picomatch @@ -24468,7 +24074,7 @@ 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.12): dependencies: tailwindcss: 4.1.12 optionalDependencies: @@ -24579,14 +24185,14 @@ snapshots: optionalDependencies: '@swc/core': 1.13.3(@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: @@ -24664,7 +24270,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 @@ -24749,10 +24355,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,41 +24413,6 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.25(ioredis@5.7.0)(pg@8.16.3)(reflect-metadata@0.2.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) @@ -24886,8 +24453,6 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.20.0: {} - undici-types@6.21.0: {} undici-types@7.10.0: @@ -24995,9 +24560,9 @@ 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.5(@swc/core@1.13.3(@swc/helpers@0.5.17))(rollup@4.50.1): dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.3) + '@rollup/pluginutils': 5.2.0(rollup@4.50.1) '@swc/core': 1.13.3(@swc/helpers@0.5.17) load-tsconfig: 0.2.5 unplugin: 2.3.5 @@ -25010,12 +24575,6 @@ snapshots: 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 @@ -25043,14 +24602,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: @@ -25143,43 +24702,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.2.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.4)(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@22.13.14)(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-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): - 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@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.4)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -25200,7 +24738,7 @@ snapshots: 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) + vite: 7.1.5(@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 @@ -25215,68 +24753,41 @@ snapshots: - 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)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.4)(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.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.4)(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.4)(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.4 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.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 - optionalDependencies: - '@types/node': 22.17.2 - 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 + rollup: 4.50.1 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.3.0 fsevents: 2.3.3 @@ -25285,19 +24796,19 @@ snapshots: 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)): + vitefu@1.1.1(vite@7.1.5(@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) + vite: 7.1.5(@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.4)(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.4)(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.4)(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.4)(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 @@ -25308,19 +24819,19 @@ snapshots: expect-type: 1.2.1 magic-string: 0.30.17 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.4)(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.4)(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.4 happy-dom: 18.0.1 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: @@ -25337,11 +24848,11 @@ 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.4)(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.4)(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 @@ -25352,21 +24863,21 @@ snapshots: expect-type: 1.2.1 magic-string: 0.30.17 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.4)(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.4)(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.4 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 @@ -25385,7 +24896,7 @@ snapshots: 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.3.0)(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 @@ -25396,14 +24907,14 @@ snapshots: expect-type: 1.2.1 magic-string: 0.30.17 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: 7.1.5(@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) why-is-node-running: 2.3.0 optionalDependencies: @@ -25481,16 +24992,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 +25031,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,6 +25059,38 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webpack@5.100.2: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.25.3 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(webpack@5.100.2) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.100.2(@swc/core@1.13.3(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 @@ -25580,38 +25123,7 @@ snapshots: - esbuild - uglify-js - webpack@5.99.9: - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.25.1 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.2 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(webpack@5.99.9) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - 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 +25132,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: 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_uk_UA.md b/readme_i18n/README_uk_UA.md index 5a33fa210d..33687bbc50 100644 --- a/readme_i18n/README_uk_UA.md +++ b/readme_i18n/README_uk_UA.md @@ -42,11 +42,11 @@ - ⚠️ Цей проєкт перебуває **в дуже активній** розробці. - ⚠️ Очікуйте безліч помилок і глобальних змін. -- ⚠️ **Не використовуйте цей додаток як єдине сховище своїх фото та відео.** +- ⚠️ **Не використовуйте цей застосунок як єдине сховище своїх фото та відео.** - ⚠️ Завжди дотримуйтесь [плану резервного копіювання 3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) для ваших дорогоцінних фотографій та відео! > [!NOTE] -> Основну документацію, зокрема посібники з встановлення, можна знайти за адресою https://immich.app/. +> Основну документацію, зокрема посібники зі встановлення, можна знайти за адресою https://immich.app/. ## Посилання @@ -61,7 +61,7 @@ ## Демо -Доступ до демо-версії [тут](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 @@ | Функції | Додаток | Веб | | :------------------------------------------------------- | ------- | --- | | Завантаження та перегляд відео й фото | Так | Так | -| Автоматичне резервне копіювання при відкритті додатка | Так | Н/Д | +| Автоматичне резервне копіювання при відкритті застосунку | Так | Н/Д | | Запобігання дублюванню файлів | Так | Так | | Вибір альбомів для резервного копіювання | Так | Н/Д | | Завантаження фото та відео на локальний пристрій | Так | Так | @@ -112,7 +112,7 @@ Статус перекладів -## Активність репозитарію +## Активність репозиторію ![Діяльність](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Зображення аналітики Repobeats") diff --git a/server/.nvmrc b/server/.nvmrc index 91d5f6ff8e..e2228113dd 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.0 diff --git a/server/Dockerfile b/server/Dockerfile index bd5c55402e..e95eca7c12 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:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a 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:202509091104@sha256:d1ccbac24c84f2f8277cf85281edfca62d85d7daed6a62b8efd3a81bcd3c5e0e 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..644d605f48 --- /dev/null +++ b/server/Dockerfile.dev @@ -0,0 +1,77 @@ +# dev build +FROM ghcr.io/immich-app/base-server-dev:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a 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.3" +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..15390ae158 100755 --- a/server/bin/start.sh +++ b/server/bin/start.sh @@ -8,7 +8,7 @@ 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 c0594c1f17..ac3b0f3c1f 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.140.1", + "version": "1.142.1", "description": "", "author": "", "private": true, @@ -37,14 +37,12 @@ "@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", @@ -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.19.0" }, "overrides": { - "sharp": "^0.34.2" + "sharp": "^0.34.3" } } diff --git a/server/src/config.ts b/server/src/config.ts index 33a6f19ba1..0d1e293be8 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -191,7 +191,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', 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 171cfe7047..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', }) 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/cores/storage.core.ts b/server/src/cores/storage.core.ts index 06dde4644c..25573cb08e 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 25395000cd..755069d827 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,5 +1,6 @@ +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'; @@ -65,10 +66,18 @@ 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() - @IsArray() @ValidateNested({ each: true }) - @Type(() => AssetMetadataUpsertItemDto) + @IsArray() metadata!: AssetMetadataUpsertItemDto[]; @ApiProperty({ type: 'string', format: 'binary', required: false }) 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..ac7bf4feb2 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -199,7 +199,12 @@ export class StatisticsSearchDto extends BaseSearchDto { export class SmartSearchDto extends BaseSearchWithResultsDto { @IsString() @IsNotEmpty() - query!: string; + @Optional() + 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 0fae619e0f..c936ec52cc 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -336,6 +336,9 @@ export class SyncAckV1 {} @ExtraModel() export class SyncResetV1 {} +@ExtraModel() +export class SyncCompleteV1 {} + export type SyncItem = { [SyncEntityType.AuthUserV1]: SyncAuthUserV1; [SyncEntityType.UserV1]: SyncUserV1; @@ -382,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/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/enum.ts b/server/src/enum.ts index bf72b24a14..646138b060 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -530,6 +530,7 @@ export enum JobName { AssetGenerateThumbnails = 'AssetGenerateThumbnails', AuditLogCleanup = 'AuditLogCleanup', + AuditTableCleanup = 'AuditTableCleanup', DatabaseBackup = 'DatabaseBackup', @@ -570,8 +571,7 @@ export enum JobName { SendMail = 'SendMail', SidecarQueueAll = 'SidecarQueueAll', - SidecarDiscovery = 'SidecarDiscovery', - SidecarSync = 'SidecarSync', + SidecarCheck = 'SidecarCheck', SidecarWrite = 'SidecarWrite', SmartSearchQueueAll = 'SmartSearchQueueAll', @@ -708,6 +708,7 @@ export enum SyncEntityType { SyncAckV1 = 'SyncAckV1', SyncResetV1 = 'SyncResetV1', + SyncCompleteV1 = 'SyncCompleteV1', } export enum NotificationLevel { diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index ef1b5fe79e..471de1ac34 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -43,6 +43,18 @@ where limit $2 +-- AssetJobRepository.getForSidecarCheckJob +select + "id", + "sidecarPath", + "originalPath" +from + "asset" +where + "asset"."id" = $1::uuid +limit + $2 + -- AssetJobRepository.streamForThumbnailJob select "asset"."id", 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 3e70baa5d4..809b59df10 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -591,6 +591,7 @@ from where "user"."updateId" < $1 and "user"."updateId" > $2 + and "id" = $3 order by "user"."updateId" asc @@ -957,7 +958,7 @@ where order by "stack"."updateId" asc --- SyncRepository.people.getDeletes +-- SyncRepository.person.getDeletes select "id", "personId" @@ -970,7 +971,7 @@ where order by "person_audit"."id" asc --- SyncRepository.people.getUpserts +-- SyncRepository.person.getUpserts select "id", "createdAt", diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index f7715b027c..ca1291b852 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -39,10 +39,8 @@ export class AssetJobRepository { return this.db .selectFrom('asset') .where('asset.id', '=', asUuid(id)) - .select((eb) => [ - 'id', - 'sidecarPath', - 'originalPath', + .select(['id', 'sidecarPath', 'originalPath']) + .select((eb) => jsonArrayFrom( eb .selectFrom('tag') @@ -50,7 +48,17 @@ export class AssetJobRepository { .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId') .whereRef('asset.id', '=', 'tag_asset.assetsId'), ).as('tags'), - ]) + ) + .limit(1) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + getForSidecarCheckJob(id: string) { + return this.db + .selectFrom('asset') + .where('asset.id', '=', asUuid(id)) + .select(['id', 'sidecarPath', 'originalPath']) .limit(1) .executeTakeFirst(); } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ae595e35ae..5c3bd8996c 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -60,6 +60,7 @@ interface AssetBuilderOptions { status?: AssetStatus; assetType?: AssetType; visibility?: AssetVisibility; + withCoordinates?: boolean; } export interface TimeBucketOptions extends AssetBuilderOptions { @@ -628,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!)) @@ -701,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/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/media.repository.ts b/server/src/repositories/media.repository.ts index 6266acf0ed..adbb8c5879 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -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/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 398d49bd5d..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'; @@ -62,7 +62,7 @@ export class SyncRepository { partnerAsset: PartnerAssetsSync; partnerAssetExif: PartnerAssetExifsSync; partnerStack: PartnerStackSync; - people: PersonSync; + person: PersonSync; stack: StackSync; user: UserSync; userMetadata: UserMetadataSync; @@ -84,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); @@ -117,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`); @@ -150,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; @@ -286,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; @@ -334,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; @@ -371,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) @@ -387,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(); } } @@ -400,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) @@ -431,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) @@ -473,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) @@ -505,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) @@ -537,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; @@ -616,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) @@ -664,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(); @@ -679,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) @@ -698,6 +756,10 @@ class AssetMetadataSync extends BaseSync { .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) diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 48f454d455..c8474cda03 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -166,6 +166,7 @@ export interface DB { api_key: ApiKeyTable; asset: AssetTable; + asset_audit: AssetAuditTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; asset_face_audit: AssetFaceAuditTable; @@ -173,7 +174,6 @@ export interface DB { asset_metadata: AssetMetadataTable; asset_metadata_audit: AssetMetadataAuditTable; asset_job_status: AssetJobStatusTable; - asset_audit: AssetAuditTable; audit: AuditTable; 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.service.spec.ts b/server/src/services/asset.service.spec.ts index 7b29b8ab96..93861149c3 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 725e3cff15..6cb0219745 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -115,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); } } diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index c1cb94b64c..40dea2044c 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, }, }, ); diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 63d5fb2d06..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 }, @@ -238,11 +239,11 @@ describe(JobService.name, () => { const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [ { - item: { name: JobName.SidecarSync, data: { id: 'asset-1' } }, + item: { name: JobName.SidecarCheck, data: { id: 'asset-1' } }, jobs: [JobName.AssetExtractMetadata], }, { - item: { name: JobName.SidecarDiscovery, data: { id: 'asset-1' } }, + item: { name: JobName.SidecarCheck, data: { id: 'asset-1' } }, jobs: [JobName.AssetExtractMetadata], }, { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 0116c869c6..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 }, ); } @@ -309,8 +310,7 @@ export class JobService extends BaseService { */ private async onDone(item: JobItem) { switch (item.name) { - case JobName.SidecarSync: - case JobName.SidecarDiscovery: { + case JobName.SidecarCheck: { await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: item.data }); break; } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index edd0d46bce..64f0915698 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -527,7 +527,7 @@ describe(LibraryService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SidecarDiscovery, + name: JobName.SidecarCheck, data: { id: assetStub.external.id, source: 'upload', @@ -573,7 +573,7 @@ describe(LibraryService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SidecarDiscovery, + name: JobName.SidecarCheck, data: { id: assetStub.image.id, source: 'upload', diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index c5f6971a83..f4a1992d91 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -414,7 +414,7 @@ export class LibraryService extends BaseService { // We queue a sidecar discovery which, in turn, queues metadata extraction await this.jobRepository.queueAll( assetIds.map((assetId) => ({ - name: JobName.SidecarDiscovery, + name: JobName.SidecarCheck, data: { id: assetId, source: 'upload' }, })), ); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 1e304137e4..0adb390f6a 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1,7 +1,6 @@ import { BinaryField, ExifDateTime } from 'exiftool-vendored'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; -import { constants } from 'node:fs/promises'; import { defaults } from 'src/config'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; @@ -15,6 +14,21 @@ import { tagStub } from 'test/fixtures/tag.stub'; import { factory } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; +const forSidecarJob = ( + asset: { + id?: string; + originalPath?: string; + sidecarPath?: string | null; + } = {}, +) => { + return { + id: factory.uuid(), + originalPath: '/path/to/IMG_123.jpg', + sidecarPath: null, + ...asset, + }; +}; + const makeFaceTags = (face: Partial<{ Name: string }> = {}, orientation?: ImmichTags['Orientation']) => ({ Orientation: orientation, RegionInfo: { @@ -1457,7 +1471,7 @@ describe(MetadataService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SidecarSync, + name: JobName.SidecarCheck, data: { id: assetStub.sidecar.id }, }, ]); @@ -1471,133 +1485,65 @@ describe(MetadataService.name, () => { expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SidecarDiscovery, + name: JobName.SidecarCheck, data: { id: assetStub.image.id }, }, ]); }); }); - describe('handleSidecarSync', () => { + describe('handleSidecarCheck', () => { it('should do nothing if asset could not be found', async () => { - mocks.asset.getByIds.mockResolvedValue([]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); + mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(void 0); + + await expect(sut.handleSidecarCheck({ id: assetStub.image.id })).resolves.toBeUndefined(); + expect(mocks.asset.update).not.toHaveBeenCalled(); }); - it('should do nothing if asset has no sidecar path', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); - expect(mocks.asset.update).not.toHaveBeenCalled(); + it('should detect a new sidecar at .jpg.xmp', async () => { + const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg' }); + + 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` }); }); - it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]); - mocks.storage.checkFileExists.mockResolvedValue(true); + it('should detect a new sidecar at .xmp', async () => { + const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg' }); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.Success); - expect(mocks.storage.checkFileExists).toHaveBeenCalledWith( - `${assetStub.sidecar.originalPath}.xmp`, - constants.R_OK, - ); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.sidecar.id, - sidecarPath: assetStub.sidecar.sidecarPath, - }); - }); - - it('should set sidecar path if exists (sidecar named photo.xmp)', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt as any]); + mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(false); mocks.storage.checkFileExists.mockResolvedValueOnce(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(JobStatus.Success); - expect(mocks.storage.checkFileExists).toHaveBeenNthCalledWith( - 2, - assetStub.sidecarWithoutExt.sidecarPath, - constants.R_OK, - ); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.sidecarWithoutExt.id, - sidecarPath: assetStub.sidecarWithoutExt.sidecarPath, - }); - }); + await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - it('should set sidecar path if exists (two sidecars named photo.ext.xmp and photo.xmp, should pick photo.ext.xmp)', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]); - mocks.storage.checkFileExists.mockResolvedValueOnce(true); - mocks.storage.checkFileExists.mockResolvedValueOnce(true); - - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.Success); - expect(mocks.storage.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK); - expect(mocks.storage.checkFileExists).toHaveBeenNthCalledWith( - 2, - assetStub.sidecarWithoutExt.sidecarPath, - constants.R_OK, - ); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.sidecar.id, - sidecarPath: assetStub.sidecar.sidecarPath, - }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: '/path/to/IMG_123.xmp' }); }); it('should unset sidecar path if file does not exist anymore', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]); + const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', sidecarPath: '/path/to/IMG_123.jpg.xmp' }); + mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValue(false); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.Success); - expect(mocks.storage.checkFileExists).toHaveBeenCalledWith( - `${assetStub.sidecar.originalPath}.xmp`, - constants.R_OK, - ); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.sidecar.id, - sidecarPath: null, - }); - }); - }); + await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - describe('handleSidecarDiscovery', () => { - it('should skip hidden assets', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset as any]); - await sut.handleSidecarDiscovery({ id: assetStub.livePhotoMotionAsset.id }); - expect(mocks.storage.checkFileExists).not.toHaveBeenCalled(); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: null }); }); - it('should skip assets with a sidecar path', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]); - await sut.handleSidecarDiscovery({ id: assetStub.sidecar.id }); - expect(mocks.storage.checkFileExists).not.toHaveBeenCalled(); - }); + 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' }); + + mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); + mocks.storage.checkFileExists.mockResolvedValueOnce(true); + + await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Skipped); - it('should do nothing when a sidecar is not found ', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - mocks.storage.checkFileExists.mockResolvedValue(false); - await sut.handleSidecarDiscovery({ id: assetStub.image.id }); expect(mocks.asset.update).not.toHaveBeenCalled(); }); - - it('should update a image asset when a sidecar is found', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - mocks.storage.checkFileExists.mockResolvedValue(true); - await sut.handleSidecarDiscovery({ id: assetStub.image.id }); - expect(mocks.storage.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.image.id, - sidecarPath: '/original/path.jpg.xmp', - }); - }); - - it('should update a video asset when a sidecar is found', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.video]); - mocks.storage.checkFileExists.mockResolvedValue(true); - await sut.handleSidecarDiscovery({ id: assetStub.video.id }); - expect(mocks.storage.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.image.id, - sidecarPath: '/original/path.ext.xmp', - }); - }); }); describe('handleSidecarWrite', () => { @@ -1714,5 +1660,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 ac2b927510..7d3de76550 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -5,7 +5,7 @@ import _ from 'lodash'; import { Duration } from 'luxon'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; -import path from 'node:path'; +import { join, parse } from 'node:path'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { Asset, AssetFace } from 'src/database'; @@ -39,9 +39,9 @@ const EXIF_DATE_TAGS: Array = [ 'SubSecCreateDate', 'SubSecMediaCreateDate', 'DateTimeOriginal', + 'CreationDate', 'CreateDate', 'MediaCreateDate', - 'CreationDate', 'DateTimeCreated', 'GPSDateTime', 'DateTimeUTC', @@ -331,7 +331,7 @@ export class MetadataService extends BaseService { const assets = this.assetJobRepository.streamForSidecar(force); for await (const asset of assets) { - jobs.push({ name: force ? JobName.SidecarSync : JobName.SidecarDiscovery, data: { id: asset.id } }); + jobs.push({ name: JobName.SidecarCheck, data: { id: asset.id } }); if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await queueAll(); } @@ -342,14 +342,37 @@ export class MetadataService extends BaseService { return JobStatus.Success; } - @OnJob({ name: JobName.SidecarSync, queue: QueueName.Sidecar }) - handleSidecarSync({ id }: JobOf): Promise { - return this.processSidecar(id, true); - } + @OnJob({ name: JobName.SidecarCheck, queue: QueueName.Sidecar }) + async handleSidecarCheck({ id }: JobOf): Promise { + const asset = await this.assetJobRepository.getForSidecarCheckJob(id); + if (!asset) { + return; + } - @OnJob({ name: JobName.SidecarDiscovery, queue: QueueName.Sidecar }) - handleSidecarDiscovery({ id }: JobOf): Promise { - return this.processSidecar(id, false); + let sidecarPath = null; + for (const candidate of this.getSidecarCandidates(asset)) { + const exists = await this.storageRepository.checkFileExists(candidate, constants.R_OK); + if (!exists) { + continue; + } + + sidecarPath = candidate; + break; + } + + const isChanged = sidecarPath !== asset.sidecarPath; + + this.logger.debug( + `Sidecar check found old=${asset.sidecarPath}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`, + ); + + if (!isChanged) { + return JobStatus.Skipped; + } + + await this.assetRepository.update({ id: asset.id, sidecarPath }); + + return JobStatus.Success; } @OnEvent({ name: 'AssetTag' }) @@ -399,6 +422,25 @@ export class MetadataService extends BaseService { return JobStatus.Success; } + private getSidecarCandidates({ sidecarPath, originalPath }: { sidecarPath: string | null; originalPath: string }) { + const candidates: string[] = []; + + if (sidecarPath) { + candidates.push(sidecarPath); + } + + const assetPath = parse(originalPath); + + candidates.push( + // IMG_123.jpg.xmp + `${originalPath}.xmp`, + // IMG_123.xmp + `${join(assetPath.dir, assetPath.name)}.xmp`, + ); + + return candidates; + } + private getImageDimensions(exifTags: ImmichTags): { width?: number; height?: number } { /* * The "true" values for width and height are a bit hidden, depending on the camera model and file format. @@ -564,7 +606,7 @@ export class MetadataService extends BaseService { checksum, ownerId: asset.ownerId, originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), - originalFileName: `${path.parse(asset.originalFileName).name}.mp4`, + originalFileName: `${parse(asset.originalFileName).name}.mp4`, visibility: AssetVisibility.Hidden, deviceAssetId: 'NONE', deviceId: 'NONE', @@ -793,7 +835,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; @@ -822,7 +868,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}`, ); @@ -905,60 +956,4 @@ export class MetadataService extends BaseService { return tags; } - - private async processSidecar(id: string, isSync: boolean): Promise { - const [asset] = await this.assetRepository.getByIds([id]); - - if (!asset) { - return JobStatus.Failed; - } - - if (isSync && !asset.sidecarPath) { - return JobStatus.Failed; - } - - if (!isSync && (asset.visibility === AssetVisibility.Hidden || asset.sidecarPath) && !asset.isExternal) { - return JobStatus.Failed; - } - - // XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp - const assetPath = path.parse(asset.originalPath); - const assetPathWithoutExt = path.join(assetPath.dir, assetPath.name); - const sidecarPathWithoutExt = `${assetPathWithoutExt}.xmp`; - const sidecarPathWithExt = `${asset.originalPath}.xmp`; - - const [sidecarPathWithExtExists, sidecarPathWithoutExtExists] = await Promise.all([ - this.storageRepository.checkFileExists(sidecarPathWithExt, constants.R_OK), - this.storageRepository.checkFileExists(sidecarPathWithoutExt, constants.R_OK), - ]); - - let sidecarPath = null; - if (sidecarPathWithExtExists) { - sidecarPath = sidecarPathWithExt; - } else if (sidecarPathWithoutExtExists) { - sidecarPath = sidecarPathWithoutExt; - } - - if (asset.isExternal) { - if (sidecarPath !== asset.sidecarPath) { - await this.assetRepository.update({ id: asset.id, sidecarPath }); - } - return JobStatus.Success; - } - - if (sidecarPath) { - this.logger.debug(`Detected sidecar at '${sidecarPath}' for asset ${asset.id}: ${asset.originalPath}`); - await this.assetRepository.update({ id: asset.id, sidecarPath }); - return JobStatus.Success; - } - - if (!isSync) { - return JobStatus.Failed; - } - - this.logger.debug(`No sidecar found for asset ${asset.id}: ${asset.originalPath}`); - await this.assetRepository.update({ id: asset.id, sidecarPath: null }); - - return JobStatus.Success; - } } 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/search.service.ts b/server/src/services/search.service.ts index b9391fed90..51a2c94338 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(machineLearning.urls, 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/sync.service.ts b/server/src/services/sync.service.ts index 677c799fb8..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, @@ -137,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), @@ -180,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] }); @@ -719,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 }); } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 20127bab15..486945546f 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], 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/types.ts b/server/src/types.ts index 2b603aeec5..ce0baa1672 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -249,7 +249,7 @@ export interface IEmailJob { } export interface INotifySignupJob extends IEntityJob { - tempPassword?: string; + password?: string; } export interface INotifyAlbumInviteJob extends IEntityJob { @@ -275,6 +275,9 @@ export interface QueueStatus { } export type JobItem = + // Audit + | { name: JobName.AuditTableCleanup; data?: IBaseJob } + // Backups | { name: JobName.DatabaseBackup; data?: IBaseJob } @@ -309,8 +312,7 @@ export type JobItem = // Sidecar Scanning | { name: JobName.SidecarQueueAll; data: IBaseJob } - | { name: JobName.SidecarDiscovery; data: IEntityJob } - | { name: JobName.SidecarSync; data: IEntityJob } + | { name: JobName.SidecarCheck; data: IEntityJob } | { name: JobName.SidecarWrite; data: ISidecarWriteJob } // Facial Recognition @@ -397,8 +399,8 @@ export interface VectorUpdateResult { } export interface ImmichFile extends Express.Multer.File { - /** sha1 hash of file */ uuid: string; + /** sha1 hash of file */ checksum: Buffer; } diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 066996ead5..dd1f98d6eb 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -35,7 +35,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 index 84353883a2..8ba9630520 100644 --- a/server/test/medium/specs/sync/sync-asset-metadata.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts @@ -26,7 +26,6 @@ describe(SyncEntityType.AssetMetadataV1, () => { await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -37,10 +36,11 @@ describe(SyncEntityType.AssetMetadataV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetMetadataV1]); }); it('should update asset metadata', async () => { @@ -51,7 +51,6 @@ describe(SyncEntityType.AssetMetadataV1, () => { await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -62,6 +61,7 @@ describe(SyncEntityType.AssetMetadataV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -79,10 +79,11 @@ describe(SyncEntityType.AssetMetadataV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, updatedResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetMetadataV1]); }); }); @@ -95,7 +96,6 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -106,6 +106,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -121,6 +122,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { }, 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/small.factory.ts b/server/test/small.factory.ts index 8b44b6eddc..04654552a3 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -1,4 +1,3 @@ -import { randomUUID } from 'node:crypto'; import { Activity, ApiKey, @@ -17,14 +16,15 @@ import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { OnThisDayData, UserMetadataItem } from 'src/types'; +import { v4, v7 } from 'uuid'; -export const newUuid = () => 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..e2228113dd 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -22.18.0 +22.19.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..54337ea78f 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, diff --git a/web/package.json b/web/package.json index a848f0102a..ee88d1dedd 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.140.1", + "version": "1.142.1", "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.28.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -53,6 +53,7 @@ "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-i18n": "^4.0.1", @@ -62,9 +63,8 @@ "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", @@ -82,7 +82,6 @@ "@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", @@ -102,13 +101,12 @@ "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.19.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/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/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 93% 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..d7b73430e3 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')}

    {#each options.secondOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte b/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte similarity index 74% rename from web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte rename to web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte index c1255c252d..cb59806524 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte +++ b/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte @@ -3,13 +3,13 @@
    -

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

    +

    {$t('other_variables')}

    -

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

    +

    {$t('filename')}

    • {`{{filename}}`} - IMG_123
    • {`{{ext}}`} - jpg
    • @@ -17,14 +17,14 @@
    -

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

    +

    {$t('filetype')}

    • {`{{filetype}}`} - VID or IMG
    • {`{{filetypefull}}`} - VIDEO or IMAGE
    -

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

    +

    {$t('other')}

    • {`{{assetId}}`} - Asset ID
    • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
    • diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-settings/TemplateSettings.svelte similarity index 91% rename from web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte rename to web/src/lib/components/admin-settings/TemplateSettings.svelte index a337aab2ae..a4ed80d9d9 100644 --- a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte +++ b/web/src/lib/components/admin-settings/TemplateSettings.svelte @@ -1,13 +1,11 @@ - + {$t('connected_to')} {castManager.receiverName} diff --git a/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte index 9ba6f24f9a..a15a787e64 100644 --- a/web/src/lib/components/assets/broken-asset.svelte +++ b/web/src/lib/components/assets/broken-asset.svelte @@ -1,5 +1,5 @@ - - - {#if title} - {title} - {/if} - {#if desc} - {desc} - {/if} - - diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index 30f3828913..7f829d60a5 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -7,14 +7,13 @@ import { zoomImageToBase64 } from '$lib/utils/people-utils'; import { getPersonNameWithHiddenValue } from '$lib/utils/person'; import { AssetTypeEnum, getAllPeople, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk'; - import { IconButton } from '@immich/ui'; + import { IconButton, LoadingSpinner } from '@immich/ui'; import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { linear } from 'svelte/easing'; import { fly } from 'svelte/transition'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; - import LoadingSpinner from '../shared-components/loading-spinner.svelte'; interface Props { editedFace: AssetFaceResponseDto; diff --git a/web/src/lib/components/faces-page/manage-people-visibility.svelte b/web/src/lib/components/faces-page/manage-people-visibility.svelte index b8154bc36e..5d03b7e558 100644 --- a/web/src/lib/components/faces-page/manage-people-visibility.svelte +++ b/web/src/lib/components/faces-page/manage-people-visibility.svelte @@ -110,7 +110,7 @@
      import { goto } from '$app/navigation'; import { page } from '$app/state'; - import Icon from '$lib/components/elements/icon.svelte'; import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button, IconButton, modalManager } from '@immich/ui'; + import { Button, Icon, IconButton, modalManager } from '@immich/ui'; import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -127,7 +126,7 @@
      - +
      {#if selectedPeople.length === 1}
      diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 51fd53b7d5..3d865223ca 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -1,10 +1,10 @@ - - - {#snippet children({ message, tag })} - {#if tag === 'b'} - {message} - {/if} - {/snippet} - diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/jobs/JobTile.svelte similarity index 83% rename from web/src/lib/components/admin-page/jobs/job-tile.svelte rename to web/src/lib/components/jobs/JobTile.svelte index d2e0ca3ac4..7823e3ee9b 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/jobs/JobTile.svelte @@ -1,9 +1,8 @@ diff --git a/web/src/lib/components/layouts/ErrorLayout.svelte b/web/src/lib/components/layouts/ErrorLayout.svelte index f2f59457f7..1c314bca60 100644 --- a/web/src/lib/components/layouts/ErrorLayout.svelte +++ b/web/src/lib/components/layouts/ErrorLayout.svelte @@ -1,8 +1,7 @@ @@ -18,8 +18,8 @@
      - -

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

      + +

      {$t('light')}

      diff --git a/web/src/lib/components/pages/SharedLinkPage.svelte b/web/src/lib/components/pages/SharedLinkPage.svelte index a3f5599077..3807f5ef0f 100644 --- a/web/src/lib/components/pages/SharedLinkPage.svelte +++ b/web/src/lib/components/pages/SharedLinkPage.svelte @@ -3,7 +3,6 @@ import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; - import PasswordField from '$lib/components/shared-components/password-field.svelte'; import ThemeButton from '$lib/components/shared-components/theme-button.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { user } from '$lib/stores/user.store'; @@ -11,7 +10,7 @@ import { handleError } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; import { getMySharedLink, SharedLinkType, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, PasswordInput } from '@immich/ui'; import { tick } from 'svelte'; import { t } from 'svelte-i18n'; @@ -79,7 +78,7 @@
      - +
      diff --git a/web/src/lib/components/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte index c04b5f39d0..d5036dbe9f 100644 --- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte +++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte @@ -1,5 +1,5 @@ - -
      -
      -
      - - -
      - -
      - - -
      - -
      - - -
      - -
      - -
      -
      -
      diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index d369068a2c..e8c1a35dd7 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -4,6 +4,7 @@ import type { Action } from '$lib/components/asset-viewer/actions/action'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; + import Portal from '$lib/elements/Portal.svelte'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; @@ -24,7 +25,6 @@ import { t } from 'svelte-i18n'; import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte'; - import Portal from '../portal/portal.svelte'; interface Props { initialAssetId?: string; @@ -42,6 +42,7 @@ onReload?: (() => void) | undefined; pageHeaderOffset?: number; slidingWindowOffset?: number; + arrowNavigation?: boolean; } let { @@ -60,6 +61,7 @@ onReload = undefined, slidingWindowOffset = 0, pageHeaderOffset = 0, + arrowNavigation = true, }: Props = $props(); let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore; @@ -306,8 +308,12 @@ { shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() }, - { shortcut: { key: 'ArrowRight' }, preventDefault: false, onShortcut: focusNextAsset }, - { shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: focusPreviousAsset }, + ...(arrowNavigation + ? [ + { shortcut: { key: 'ArrowRight' }, preventDefault: false, onShortcut: focusNextAsset }, + { shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: focusPreviousAsset }, + ] + : []), ]; if (assetInteraction.selectionActive) { diff --git a/web/src/lib/components/shared-components/loading-spinner.svelte b/web/src/lib/components/shared-components/loading-spinner.svelte deleted file mode 100644 index e81d2225b7..0000000000 --- a/web/src/lib/components/shared-components/loading-spinner.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
      - - - - -
      diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 3c5bb5ece9..555181ab3b 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -8,7 +8,6 @@ - -
      - { - password = e.currentTarget.value; - onInput?.(password); - }} - /> - - {#if password.length > 0} - - {/if} -
      - - diff --git a/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte index e39bfddc2c..33228df9f2 100644 --- a/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte @@ -1,8 +1,7 @@ @@ -12,7 +11,7 @@ class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900" >
      - +

      {$t('purchase_individual_title')}

      @@ -24,17 +23,17 @@
      - +

      {$t('purchase_individual_description_1')}

      - +

      {$t('purchase_lifetime_description')}

      - +

      {$t('purchase_individual_description_2')}

      diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte index 387f01395d..a3ced9ab51 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -1,9 +1,8 @@
      - +

      {$t('purchase_activated_title')}

      {$t('purchase_activated_subtitle')}

      diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte index dc2e30980e..ee28e66bff 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -1,9 +1,8 @@ @@ -12,7 +11,7 @@ class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900" >
      - +

      {$t('purchase_server_title')}

      @@ -24,17 +23,17 @@
      - +

      {$t('purchase_server_description_1')}

      - +

      {$t('purchase_lifetime_description')}

      - +

      {$t('purchase_server_description_2')}

      diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 08ed57d70e..aa5b0ee7e4 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -59,7 +59,7 @@
      -

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

      +

      {$t('camera')}

      diff --git a/web/src/lib/components/shared-components/search-bar/search-date-section.svelte b/web/src/lib/components/shared-components/search-bar/search-date-section.svelte index ea27142074..62867805b6 100644 --- a/web/src/lib/components/shared-components/search-bar/search-date-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-date-section.svelte @@ -6,7 +6,7 @@
      -

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

      +

      {$t('place')}

      diff --git a/web/src/lib/components/shared-components/search-bar/search-media-section.svelte b/web/src/lib/components/shared-components/search-bar/search-media-section.svelte index 658944055a..3e05c370c7 100644 --- a/web/src/lib/components/shared-components/search-bar/search-media-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-media-section.svelte @@ -1,6 +1,6 @@ @@ -42,11 +42,11 @@ onclick={() => (dropdownOpen = !dropdownOpen)} > @@ -62,7 +62,7 @@ : ''}" >
      - + {title}
      diff --git a/web/src/lib/components/shared-components/side-bar/storage-space.svelte b/web/src/lib/components/shared-components/side-bar/storage-space.svelte index fdcec683e7..440cada910 100644 --- a/web/src/lib/components/shared-components/side-bar/storage-space.svelte +++ b/web/src/lib/components/shared-components/side-bar/storage-space.svelte @@ -4,9 +4,9 @@ import { userInteraction } from '$lib/stores/user.svelte'; import { requestServerInfo } from '$lib/utils/auth'; import { getByteUnitString } from '$lib/utils/byte-units'; + import { LoadingSpinner } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; - import LoadingSpinner from '../loading-spinner.svelte'; let usageClasses = $state(''); diff --git a/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte b/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte index 84ba56459e..0f98124504 100644 --- a/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte +++ b/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte @@ -87,7 +87,7 @@ bind:isSelected={isSharingSelected} > -

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

      +

      {$t('library')}

      - import Icon from '$lib/components/elements/icon.svelte'; import { TreeNode } from '$lib/utils/tree-utils'; - import { IconButton } from '@immich/ui'; + import { Icon, IconButton } from '@immich/ui'; import { mdiArrowUpLeft, mdiChevronRight } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -55,7 +54,7 @@
    • - + {parent.value} @@ -65,7 +64,7 @@
    • - +

      {node.value}

    • diff --git a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte index da8c24697b..17d0e710f7 100644 --- a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte +++ b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte @@ -1,6 +1,6 @@ - -
      -
      - { - if (asset.exifInfo?.latitude && asset.exifInfo?.longitude) { - onLocation({ latitude: asset.exifInfo?.latitude, longitude: asset.exifInfo?.longitude }); - } else { - onSelectAsset(asset); - } - }} - onSelect={() => onSelectAsset(asset)} - onMouseEvent={() => onMouseEvent(asset)} - selected={assetInteraction.hasSelectedAsset(asset.id)} - selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} - thumbnailSize={boxWidth} - readonly={hasGps} - /> - - {#if hasGps} -
      - {$t('gps')} -
      - {:else} -
      - {$t('gps_missing')} -
      - {/if} -
      - -
      -

      - {new Date(asset.localDateTime).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - })} -

      -

      - {new Date(asset.localDateTime).toLocaleTimeString(undefined, { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZone: 'UTC', - })} -

      - {#if hasGps} -

      - {asset.exifInfo?.country} -

      -

      - {asset.exifInfo?.city} -

      - {/if} -
      -
      diff --git a/web/src/lib/components/utilities-page/utilities-menu.svelte b/web/src/lib/components/utilities-page/utilities-menu.svelte index 97e205fcd1..d9e22aaa9e 100644 --- a/web/src/lib/components/utilities-page/utilities-menu.svelte +++ b/web/src/lib/components/utilities-page/utilities-menu.svelte @@ -1,6 +1,6 @@
      -

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

      +

      {$t('organize_your_library')}

      {#each links as link (link.href)} - + {link.label} {/each} diff --git a/web/src/lib/components/elements/badge.svelte b/web/src/lib/elements/Badge.svelte similarity index 100% rename from web/src/lib/components/elements/badge.svelte rename to web/src/lib/elements/Badge.svelte diff --git a/web/src/lib/components/elements/date-input.svelte b/web/src/lib/elements/DateInput.svelte similarity index 100% rename from web/src/lib/components/elements/date-input.svelte rename to web/src/lib/elements/DateInput.svelte diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/elements/Dropdown.svelte similarity index 95% rename from web/src/lib/components/elements/dropdown.svelte rename to web/src/lib/elements/Dropdown.svelte index 36e76ae716..8bc956c779 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/elements/Dropdown.svelte @@ -12,11 +12,10 @@ - {/snippet} - + {#if assetInteraction.selectionActive} diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte index cc69e52597..86fb0850af 100644 --- a/web/src/routes/(user)/explore/+page.svelte +++ b/web/src/routes/(user)/explore/+page.svelte @@ -1,18 +1,18 @@ - {/snippet} - + diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 51de0ac1ee..7aeb1bc46b 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,26 +1,26 @@
      - +
      {#if assetInteraction.selectionActive} diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 0f2284ce34..dda3c516ad 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -4,7 +4,6 @@ import { focusTrap } from '$lib/actions/focus-trap'; import { scrollMemory } from '$lib/actions/scroll-memory'; import { shortcut } from '$lib/actions/shortcut'; - import Icon from '$lib/components/elements/icon.svelte'; import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte'; import PeopleCard from '$lib/components/faces-page/people-card.svelte'; import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte'; @@ -23,7 +22,7 @@ import { handleError } from '$lib/utils/handle-error'; import { clearQueryParam } from '$lib/utils/navigation'; import { getAllPeople, getPerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button, modalManager } from '@immich/ui'; + import { Button, Icon, modalManager } from '@immich/ui'; import { mdiAccountOff, mdiEyeOutline } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -389,7 +388,7 @@ {:else}
      - +

      {$t(searchName ? 'search_no_people_named' : 'search_no_people', { values: { name: searchName } })}

      diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 807819b585..4574ed6d95 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -8,28 +8,27 @@ import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte'; - import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; - import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; - import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; - import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; - import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; - import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; - import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; - import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; - import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; - import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; - import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; - import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; - import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; - import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; + import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte'; + import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte'; + import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; + import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; + import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte'; + import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte'; + import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte'; + import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte'; + import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte'; + import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte'; + import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte'; + import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; + import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; + import Timeline from '$lib/components/timeline/Timeline.svelte'; import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; @@ -50,7 +49,7 @@ updatePerson, type PersonResponseDto, } from '@immich/sdk'; - import { modalManager } from '@immich/ui'; + import { LoadingSpinner, modalManager } from '@immich/ui'; import { mdiAccountBoxOutline, mdiAccountMultipleCheckOutline, @@ -386,7 +385,7 @@ }} > {#key person.id} - {/if} - + {/key} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 0098667873..096980abcd 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -1,26 +1,26 @@ - openFileUploadDialog()} /> {/snippet} - + {#if assetInteraction.selectionActive} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 56e58c3633..3934c3e445 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -3,25 +3,23 @@ import { page } from '$app/state'; import { shortcut } from '$lib/actions/shortcut'; import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; - import Icon from '$lib/components/elements/icon.svelte'; - import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; - import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; - import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; - import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; - import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; - import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; - import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; - import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; - import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; - import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; - import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; - import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; - import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; - import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; + import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte'; + import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte'; + import AssetJobActions from '$lib/components/timeline/actions/AssetJobActions.svelte'; + import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; + import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte'; + import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte'; + import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte'; + import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte'; + import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte'; + import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte'; + import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte'; + import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; + import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; @@ -45,7 +43,7 @@ searchSmart, type SmartSearchDto, } from '@immich/sdk'; - import { IconButton } from '@immich/ui'; + import { Icon, IconButton, LoadingSpinner } from '@immich/ui'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { tick } from 'svelte'; import { t } from 'svelte-i18n'; @@ -68,7 +66,7 @@ const assetInteraction = new AssetInteraction(); - type SearchTerms = MetadataSearchDto & Pick; + type SearchTerms = MetadataSearchDto & Pick; let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY)); let smartSearchEnabled = $derived($featureFlags.loaded && $featureFlags.smartSearch); let terms = $derived(searchQuery ? JSON.parse(searchQuery) : {}); @@ -164,7 +162,7 @@ try { const { albums, assets } = - 'query' in searchDto && smartSearchEnabled + ('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled ? await searchSmart({ smartSearchDto: searchDto }) : await searchAssets({ metadataSearchDto: searchDto }); @@ -210,6 +208,7 @@ tagIds: $t('tags'), originalFileName: $t('file_name'), description: $t('description'), + queryAssetId: $t('query_asset_id'), }; return keyMap[key] || key; } @@ -367,11 +366,11 @@ > {#if searchResultAlbums.length > 0}
      -
      {$t('albums').toUpperCase()}
      +
      {$t('albums')}
      -
      - {$t('photos_and_videos').toUpperCase()} +
      + {$t('photos_and_videos')}
      {/if} @@ -389,7 +388,7 @@ {:else if !isLoading}
      - +

      {$t('no_results')}

      {$t('no_results_description')}

      @@ -398,7 +397,7 @@ {#if isLoading}
      - +
      {/if} diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte index 2f732646ec..ea1c47f0f8 100644 --- a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte +++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte @@ -1,7 +1,6 @@ @@ -224,11 +139,11 @@ {#snippet buttons()}
      - {#if filteredAssets.length > 0} - - {/if} +
      -

      {$t('selected_gps_coordinates')}

      + {$t('location_picker_choose_on_map')} + + +
      {/snippet} -
      -
      - -
      - -
      - - -
      -
      - {#if isLoading}
      {/if} - {#if filteredAssets && filteredAssets.length > 0} -
      - {#each filteredAssets as asset (asset.id)} - handleSelectAssets(asset)} - onMouseEvent={(asset) => assetMouseEventHandler(asset)} - onLocation={(selected) => { - location = selected; - locationUpdated = true; - setTimeout(() => { - locationUpdated = false; - }, 1000); - }} - /> - {/each} -
      - {:else} -
      - {#if partialDate == null} - - {:else if showOnlyAssetsWithoutLocation && filteredAssets.length === 0 && assets.length > 0} - + + {#snippet customLayout(asset: TimelineAsset)} + {#if hasGps(asset)} +
      + {asset.city || $t('gps')} +
      {:else} - +
      + {$t('gps_missing')} +
      {/if} -
      - {/if} + {/snippet} + {#snippet empty()} + {}} /> + {/snippet} + diff --git a/web/src/routes/(user)/utilities/geolocation/+page.ts b/web/src/routes/(user)/utilities/geolocation/+page.ts index f5c227a7ef..1ada22a237 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.ts +++ b/web/src/routes/(user)/utilities/geolocation/+page.ts @@ -1,15 +1,12 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getQueryValue } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { await authenticate(url); - const partialDate = getQueryValue('date'); const $t = await getFormatter(); return { - partialDate, meta: { title: $t('manage_geolocation'), }, diff --git a/web/src/routes/(user)/utilities/geolocation/photos/[photoId]/+page.ts b/web/src/routes/(user)/utilities/geolocation/photos/[photoId]/+page.ts new file mode 100644 index 0000000000..17fd84097f --- /dev/null +++ b/web/src/routes/(user)/utilities/geolocation/photos/[photoId]/+page.ts @@ -0,0 +1,8 @@ +import { AppRoute } from '$lib/constants'; +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +export const load = (({ params }) => { + const photoId = params.photoId; + return redirect(302, `${AppRoute.PHOTOS}/${photoId}`); +}) satisfies PageLoad; diff --git a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte index 35c101492c..40c70c9df2 100644 --- a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,8 +1,8 @@